From 96601980c3838ba7e38859330dfcc3d8183b35c1 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Tue, 27 Jan 2026 10:27:23 +0100 Subject: [PATCH] fix: handle URLs already ending in /jsonrpc.php NewClient() now detects when the provided URL already ends with /jsonrpc.php and avoids appending it again. This prevents double-path issues like /jsonrpc.php/jsonrpc.php. Co-Authored-By: Claude Opus 4.5 --- .beads/export-state/fbbdf412d0fd5173.json | 6 +++--- .beads/issues.jsonl | 1 + client.go | 8 +++++++- client_test.go | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.beads/export-state/fbbdf412d0fd5173.json b/.beads/export-state/fbbdf412d0fd5173.json index 661ee9d..8c57567 100644 --- a/.beads/export-state/fbbdf412d0fd5173.json +++ b/.beads/export-state/fbbdf412d0fd5173.json @@ -1,6 +1,6 @@ { "worktree_root": "/home/oli/Dev/kanboard-api", - "last_export_commit": "169f81c4c49391498652aa3534bdc4d8fde7e7c2", - "last_export_time": "2026-01-27T10:21:24.788588574+01:00", - "jsonl_hash": "75e9b1d700551c2a9307c7c3996c3a005f64dde4fe45621a6e7e94b709b7212e" + "last_export_commit": "f8daa20ddd0d8ccae6ff2bbcd619a5f2b9017847", + "last_export_time": "2026-01-27T10:27:23.906334189+01:00", + "jsonl_hash": "76cece249c29be2a46702a269bddea03d105be2c4ad48d22459d3dda9be4aae6" } \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ed4d9e5..2751e36 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,5 +1,6 @@ {"id":"kanboard-7es","title":"JSON-RPC Request-ID: Zufälligen Wert statt fester 1 verwenden","description":"## Kontext\n\nIn jedem JSON-RPC Request an die Kanboard-API wird im Root-Objekt ein Feld `id` mitgeliefert. Dieses dient dazu, bei asynchroner Kommunikation Request und Response einander zuordnen zu können – die API liefert diese ID in der Antwort zurück.\n\n**Aktuell:** Die ID ist fest auf `1` gesetzt.\n\n## Anforderung\n\n1. **Wenn keine ID von außen gesetzt wird:** Die Library soll intern einen zufälligen Wert generieren\n2. **API-Dokumentation prüfen:** Welche Werte sind erlaubt? Welche Größenordnung? (vermutlich Integer)\n3. **Signatur beibehalten:** Die öffentliche API der Library-Funktionen soll unverändert bleiben\n4. **Interne Generierung:** Die Library bestimmt selbst einen zufälligen Wert\n\n## Implementierungshinweise\n\n- Prüfen: Kanboard JSON-RPC Dokumentation bezüglich erlaubter ID-Werte\n- Vermutlich: `int64` oder `int32` Range\n- Zufallsgenerator: `math/rand` mit Seed oder `crypto/rand` für bessere Verteilung\n- Ggf. bestehende `requestIDCounter` in `jsonrpc.go` (Zeile 40) anpassen oder ersetzen\n\n## Beispiel\n\n**Vorher (immer gleich):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 1, \"params\": {...}}\n```\n\n**Nachher (zufällig):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 847291536, \"params\": {...}}\n```\n\n## Referenz\n\n- Datei: `jsonrpc.go`\n- Zeile 17: `ID int64 \\`json:\"id\"\\``\n- Zeile 40: `requestIDCounter` (existiert bereits)","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T17:44:51.566737509+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:21:06.415129879+01:00","closed_at":"2026-01-27T10:21:06.415129879+01:00","close_reason":"Closed"} {"id":"kanboard-9wa","title":"Support custom authentication header name","description":"## Description\n\nKanboard supports using an alternative HTTP header for authentication when the server has specific configuration requirements.\n\nCurrently, authentication uses the standard `Authorization` header via Go's `SetBasicAuth()`. This needs to be configurable so users can specify a custom header name (e.g., `X-API-Auth`).\n\n## Requirements\n\n- Add an optional configuration parameter for custom auth header name\n- Default to standard `Authorization` header if not specified\n- When custom header is set, use that header name instead of `Authorization`\n- The header value format should remain the same (Basic Auth base64-encoded credentials)\n\n## Acceptance Criteria\n\n- [ ] New client configuration method (e.g., `WithAuthHeader(headerName string)`)\n- [ ] Default behavior unchanged when no custom header specified\n- [ ] Custom header name is used when configured\n- [ ] Works with both API token and basic auth\n- [ ] Tests cover default and custom header scenarios\n\n## Reference\n\nKanboard API documentation: \"You can use an alternative HTTP header for authentication if your server has a very specific configuration.\"","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T18:08:31.507616093+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-23T18:26:50.40804952+01:00","closed_at":"2026-01-23T18:26:50.40804952+01:00","close_reason":"Closed"} +{"id":"kanboard-a7x","title":"Handle URLs already ending in /jsonrpc.php","description":"## Context\n\nThe `NewClient()` function in `client.go` always appends `/jsonrpc.php` to the base URL. This causes issues when users pass a URL that already includes the endpoint path.\n\n## Problem\n\nIf a user calls:\n```go\nclient := kanboard.NewClient(\"https://example.com/jsonrpc.php\")\n```\n\nThe resulting endpoint becomes `https://example.com/jsonrpc.php/jsonrpc.php`, which fails.\n\n## Solution\n\nModify `NewClient()` to detect and handle URLs that already end in `/jsonrpc.php`:\n\n```go\nfunc NewClient(baseURL string) *Client {\n // Ensure no trailing slash\n baseURL = strings.TrimSuffix(baseURL, \"/\")\n\n // Handle URLs that already include /jsonrpc.php\n endpoint := baseURL\n if !strings.HasSuffix(baseURL, \"/jsonrpc.php\") {\n endpoint = baseURL + \"/jsonrpc.php\"\n }\n\n c := \u0026Client{\n baseURL: baseURL,\n endpoint: endpoint,\n }\n // ... rest unchanged\n}\n```\n\n## Files to Modify\n\n- `client.go` - Update `NewClient()` to check for existing `/jsonrpc.php` suffix\n\n## Acceptance Criteria\n\n- [ ] `NewClient(\"https://example.com\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/jsonrpc.php\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/kanboard/jsonrpc.php\")` → endpoint `https://example.com/kanboard/jsonrpc.php`\n- [ ] Tests written and passing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T10:25:51.077352962+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:27:15.189568683+01:00","closed_at":"2026-01-27T10:27:15.189568683+01:00","close_reason":"Closed"} {"id":"kanboard-api-0fz","title":"Implement Category API methods","description":"Implement direct API methods for category operations.\n\n## Methods to implement\n- GetAllCategories(ctx, projectID int) ([]Category, error) - getAllCategories\n- GetCategory(ctx, categoryID int) (*Category, error) - getCategory (Nice-to-have)\n\n## Files to create\n- categories.go\n\n## Acceptance criteria\n- Proper error handling\n- Returns empty slice when no categories exist","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.6133153+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:25:07.250066801+01:00","closed_at":"2026-01-15T18:25:07.250066801+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-0fz","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.161416595+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-0fz","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.226963473+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-16r","title":"Implement Tag API methods (CRITICAL)","description":"Implement direct API methods for tag operations. Tags are CRITICAL - heavily used.\n\n## Direct Client methods (Must-have)\n- GetTaskTags(ctx, taskID int) (map[int]string, error) - getTaskTags\n- SetTaskTags(ctx, projectID, taskID int, tags []string) error - setTaskTags\n\n## Direct Client methods (Nice-to-have)\n- GetAllTags(ctx) ([]Tag, error) - getAllTags\n- GetTagsByProject(ctx, projectID int) ([]Tag, error) - getTagsByProject\n- CreateTag(ctx, projectID int, name, colorID string) (int, error) - createTag\n- UpdateTag(ctx, tagID int, name, colorID string) error - updateTag\n- RemoveTag(ctx, tagID int) error - removeTag\n\n## Files to create\n- tags.go\n\n## IMPORTANT NOTE\nsetTaskTags REPLACES ALL tags. Individual add/remove requires read-modify-write pattern (implemented in TaskScope).\n\n## Acceptance criteria\n- GetTaskTags returns map[tagID]tagName\n- SetTaskTags accepts tag names (auto-creates if needed)","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.526810135+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:20:17.392248254+01:00","closed_at":"2026-01-15T18:20:17.392248254+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-16r","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.223137796+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-16r","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.402237867+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-25y","title":"Implement Timestamp type with JSON handling","description":"Implement custom Timestamp type that handles Kanboard's Unix timestamp format.\n\n## Requirements\n- Timestamp struct wrapping time.Time\n- UnmarshalJSON supporting:\n - Unix timestamps as integers\n - Empty strings and \"0\" as zero time\n - Zero value (0) as zero time\n- MarshalJSON returning Unix timestamp or 0 for zero time\n\n## Files to create\n- timestamp.go\n\n## Acceptance criteria\n- Correctly parses integer Unix timestamps\n- Handles empty strings and \"0\" strings\n- Zero time marshals to 0\n- Non-zero time marshals to Unix timestamp","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.0044989+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:15:27.299644047+01:00","closed_at":"2026-01-15T18:15:27.299644047+01:00","close_reason":"Closed"} diff --git a/client.go b/client.go index 156ae2d..fe41906 100644 --- a/client.go +++ b/client.go @@ -29,9 +29,15 @@ func NewClient(baseURL string) *Client { // Ensure no trailing slash baseURL = strings.TrimSuffix(baseURL, "/") + // Handle URLs that already include /jsonrpc.php + endpoint := baseURL + if !strings.HasSuffix(baseURL, "/jsonrpc.php") { + endpoint = baseURL + "/jsonrpc.php" + } + c := &Client{ baseURL: baseURL, - endpoint: baseURL + "/jsonrpc.php", + endpoint: endpoint, } c.httpClient = &http.Client{ diff --git a/client_test.go b/client_test.go index 5d37fb9..c4898b6 100644 --- a/client_test.go +++ b/client_test.go @@ -44,6 +44,22 @@ func TestNewClient_Subdirectory(t *testing.T) { } } +func TestNewClient_WithJsonrpcPhpSuffix(t *testing.T) { + client := NewClient("https://example.com/jsonrpc.php") + + if client.endpoint != "https://example.com/jsonrpc.php" { + t.Errorf("expected endpoint='https://example.com/jsonrpc.php', got %s", client.endpoint) + } +} + +func TestNewClient_SubdirectoryWithJsonrpcPhpSuffix(t *testing.T) { + client := NewClient("https://example.com/kanboard/jsonrpc.php") + + if client.endpoint != "https://example.com/kanboard/jsonrpc.php" { + t.Errorf("expected endpoint='https://example.com/kanboard/jsonrpc.php', got %s", client.endpoint) + } +} + func TestDefaultTimeout(t *testing.T) { if DefaultTimeout != 30*time.Second { t.Errorf("expected DefaultTimeout=30s, got %v", DefaultTimeout)