Skip to content

Commit 30e73b7

Browse files
authored
fix: bc for api (#63)
1 parent edc607e commit 30e73b7

4 files changed

Lines changed: 70 additions & 19 deletions

File tree

internal/api/client.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"net/http"
1313
"net/url"
1414
"os"
15+
"strings"
16+
"sync"
1517
"syscall"
1618
"time"
1719
)
@@ -21,6 +23,8 @@ type Client struct {
2123
baseURL string
2224
apiKey string
2325
httpClient *http.Client
26+
prefix string // resolved API prefix: "/v1" or "/api"
27+
prefixOnce sync.Once // ensures prefix detection runs once
2428
}
2529

2630
// New creates an API client.
@@ -32,6 +36,16 @@ func New(baseURL, apiKey string) *Client {
3236
}
3337
}
3438

39+
// newWithPrefix creates a client with a pre-set prefix (for testing).
40+
func newWithPrefix(baseURL, apiKey, prefix string) *Client {
41+
return &Client{
42+
baseURL: baseURL,
43+
apiKey: apiKey,
44+
httpClient: buildHTTPClient(),
45+
prefix: prefix,
46+
}
47+
}
48+
3549
func buildHTTPClient() *http.Client {
3650
client := &http.Client{Timeout: 30 * time.Second}
3751
f := os.Getenv("SSL_CERT_FILE")
@@ -93,9 +107,44 @@ func (c *Client) networkError(err error) error {
93107
return fmt.Errorf("could not reach gateway at %s: %w", host, err)
94108
}
95109

110+
// resolvePrefix probes the server once to determine whether it supports
111+
// /v1 (new) or /api (legacy). All subsequent requests use the resolved prefix.
112+
func (c *Client) resolvePrefix(ctx context.Context) {
113+
if c.prefix != "" {
114+
return
115+
}
116+
c.prefixOnce.Do(func() {
117+
c.prefix = "/v1"
118+
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/v1/health", nil)
119+
if err != nil {
120+
return
121+
}
122+
if c.apiKey != "" {
123+
req.Header.Set("Authorization", "Bearer "+c.apiKey)
124+
}
125+
resp, err := c.httpClient.Do(req)
126+
if resp != nil {
127+
resp.Body.Close()
128+
}
129+
if err != nil || resp.StatusCode == http.StatusNotFound {
130+
c.prefix = "/api"
131+
}
132+
})
133+
}
134+
135+
// applyPrefix replaces the /v1 prefix in path with the resolved prefix.
136+
func (c *Client) applyPrefix(path string) string {
137+
if c.prefix == "/api" && strings.HasPrefix(path, "/v1/") {
138+
return "/api" + path[3:]
139+
}
140+
return path
141+
}
142+
96143
// do executes an HTTP request and decodes the JSON response.
97144
// For 204 responses, result should be nil.
98145
func (c *Client) do(ctx context.Context, method, path string, body any, result any) error {
146+
c.resolvePrefix(ctx)
147+
path = c.applyPrefix(path)
99148
var bodyReader io.Reader
100149
if body != nil {
101150
data, err := json.Marshal(body)

internal/api/client_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestDoSetsAuthHeader(t *testing.T) {
1818
}))
1919
defer srv.Close()
2020

21-
client := New(srv.URL, "oc_testkey123")
21+
client := newWithPrefix(srv.URL, "oc_testkey123", "/v1")
2222
var result map[string]any
2323
_ = client.do(context.Background(), http.MethodGet, "/test", nil, &result)
2424

@@ -36,7 +36,7 @@ func TestDoOmitsAuthHeaderWhenNoKey(t *testing.T) {
3636
}))
3737
defer srv.Close()
3838

39-
client := New(srv.URL, "")
39+
client := newWithPrefix(srv.URL, "", "/v1")
4040
var result map[string]any
4141
_ = client.do(context.Background(), http.MethodGet, "/test", nil, &result)
4242

@@ -56,7 +56,7 @@ func TestDoSendsJSONBody(t *testing.T) {
5656
}))
5757
defer srv.Close()
5858

59-
client := New(srv.URL, "")
59+
client := newWithPrefix(srv.URL, "", "/v1")
6060
body := map[string]string{"name": "test"}
6161
var result map[string]any
6262
_ = client.do(context.Background(), http.MethodPost, "/test", body, &result)
@@ -78,7 +78,7 @@ func TestDoNoContentTypeWithoutBody(t *testing.T) {
7878
}))
7979
defer srv.Close()
8080

81-
client := New(srv.URL, "")
81+
client := newWithPrefix(srv.URL, "", "/v1")
8282
var result map[string]any
8383
_ = client.do(context.Background(), http.MethodGet, "/test", nil, &result)
8484

@@ -93,7 +93,7 @@ func TestDoHandles204(t *testing.T) {
9393
}))
9494
defer srv.Close()
9595

96-
client := New(srv.URL, "")
96+
client := newWithPrefix(srv.URL, "", "/v1")
9797
err := client.do(context.Background(), http.MethodDelete, "/test", nil, nil)
9898
if err != nil {
9999
t.Errorf("204 should not return error, got %v", err)
@@ -107,7 +107,7 @@ func TestDoDecodesSuccessResponse(t *testing.T) {
107107
}))
108108
defer srv.Close()
109109

110-
client := New(srv.URL, "")
110+
client := newWithPrefix(srv.URL, "", "/v1")
111111
var agent Agent
112112
err := client.do(context.Background(), http.MethodGet, "/test", nil, &agent)
113113
if err != nil {
@@ -125,7 +125,7 @@ func TestDoReturnsAPIErrorWithMessage(t *testing.T) {
125125
}))
126126
defer srv.Close()
127127

128-
client := New(srv.URL, "")
128+
client := newWithPrefix(srv.URL, "", "/v1")
129129
err := client.do(context.Background(), http.MethodPost, "/test", nil, nil)
130130

131131
var apiErr *APIError
@@ -147,7 +147,7 @@ func TestDoReturnsAPIErrorFallbackMessage(t *testing.T) {
147147
}))
148148
defer srv.Close()
149149

150-
client := New(srv.URL, "")
150+
client := newWithPrefix(srv.URL, "", "/v1")
151151
err := client.do(context.Background(), http.MethodGet, "/test", nil, nil)
152152

153153
var apiErr *APIError
@@ -169,7 +169,7 @@ func TestDoReturnsAPIError401(t *testing.T) {
169169
}))
170170
defer srv.Close()
171171

172-
client := New(srv.URL, "")
172+
client := newWithPrefix(srv.URL, "", "/v1")
173173
err := client.do(context.Background(), http.MethodGet, "/test", nil, nil)
174174

175175
var apiErr *APIError
@@ -188,7 +188,7 @@ func TestDoReturnsAPIError404(t *testing.T) {
188188
}))
189189
defer srv.Close()
190190

191-
client := New(srv.URL, "")
191+
client := newWithPrefix(srv.URL, "", "/v1")
192192
err := client.do(context.Background(), http.MethodGet, "/test", nil, nil)
193193

194194
var apiErr *APIError
@@ -219,7 +219,7 @@ func TestDoSendsCorrectMethod(t *testing.T) {
219219
}))
220220
defer srv.Close()
221221

222-
client := New(srv.URL, "")
222+
client := newWithPrefix(srv.URL, "", "/v1")
223223
_ = client.do(context.Background(), method, "/test", nil, nil)
224224

225225
if gotMethod != method {
@@ -238,7 +238,7 @@ func TestDoSendsCorrectPath(t *testing.T) {
238238
}))
239239
defer srv.Close()
240240

241-
client := New(srv.URL, "")
241+
client := newWithPrefix(srv.URL, "", "/v1")
242242
_ = client.do(context.Background(), http.MethodGet, "/v1/agents", nil, nil)
243243

244244
if gotPath != "/v1/agents" {

internal/api/projects_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestListProjects(t *testing.T) {
2323
}))
2424
defer srv.Close()
2525

26-
client := New(srv.URL, "oc_test")
26+
client := newWithPrefix(srv.URL, "oc_test", "/v1")
2727
projects, err := client.ListProjects(context.Background())
2828
if err != nil {
2929
t.Fatal(err)
@@ -46,7 +46,7 @@ func TestGetProject(t *testing.T) {
4646
}))
4747
defer srv.Close()
4848

49-
client := New(srv.URL, "oc_test")
49+
client := newWithPrefix(srv.URL, "oc_test", "/v1")
5050
project, err := client.GetProject(context.Background(), "p1")
5151
if err != nil {
5252
t.Fatal(err)
@@ -71,7 +71,7 @@ func TestCreateProject(t *testing.T) {
7171
}))
7272
defer srv.Close()
7373

74-
client := New(srv.URL, "oc_test")
74+
client := newWithPrefix(srv.URL, "oc_test", "/v1")
7575
project, err := client.CreateProject(context.Background(), CreateProjectInput{Name: "Beta"})
7676
if err != nil {
7777
t.Fatal(err)
@@ -97,7 +97,7 @@ func TestUpdateProject(t *testing.T) {
9797
}))
9898
defer srv.Close()
9999

100-
client := New(srv.URL, "oc_test")
100+
client := newWithPrefix(srv.URL, "oc_test", "/v1")
101101
name := "Renamed"
102102
project, err := client.UpdateProject(context.Background(), "p1", UpdateProjectInput{Name: &name})
103103
if err != nil {
@@ -120,7 +120,7 @@ func TestDeleteProject(t *testing.T) {
120120
}))
121121
defer srv.Close()
122122

123-
client := New(srv.URL, "oc_test")
123+
client := newWithPrefix(srv.URL, "oc_test", "/v1")
124124
err := client.DeleteProject(context.Background(), "p1")
125125
if err != nil {
126126
t.Errorf("expected no error, got %v", err)
@@ -134,7 +134,7 @@ func TestListProjectsError(t *testing.T) {
134134
}))
135135
defer srv.Close()
136136

137-
client := New(srv.URL, "oc_test")
137+
client := newWithPrefix(srv.URL, "oc_test", "/v1")
138138
_, err := client.ListProjects(context.Background())
139139
if err == nil {
140140
t.Error("expected error")

internal/api/skill.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99

1010
// GetGatewaySkill fetches the gateway skill markdown from the API.
1111
func (c *Client) GetGatewaySkill(ctx context.Context) (string, error) {
12-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/v1/skill/gateway", nil)
12+
c.resolvePrefix(ctx)
13+
path := c.applyPrefix("/v1/skill/gateway")
14+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
1315
if err != nil {
1416
return "", fmt.Errorf("creating request: %w", err)
1517
}

0 commit comments

Comments
 (0)