Skip to content

Commit d2e954c

Browse files
committed
fix(api): require selector on v1 query GET
Signed-off-by: Tjark Gunnar Rasche <trasche@nvidia.com>
1 parent f0dcc7e commit d2e954c

4 files changed

Lines changed: 76 additions & 22 deletions

File tree

api/aicr/v1/server.yaml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -646,9 +646,10 @@ paths:
646646
useOpenKernelModules: true
647647
"400":
648648
description: >
649-
Invalid request: malformed criteria, allowlist violation, a resolved
650-
profile (which requires `/v2/query`), or a stated criteria dimension
651-
not honored by any applicable recipe overlay (uncovered dimension).
649+
Invalid request: an omitted selector, malformed criteria, allowlist
650+
violation, a resolved profile (which requires `/v2/query`), or a stated
651+
criteria dimension not honored by any applicable recipe overlay
652+
(uncovered dimension).
652653
Uncovered-dimension errors carry a machine-readable
653654
`details.uncovered` array (dimension, requestedValue,
654655
validCompletions). Snapshot-driven resolution (CLI `--snapshot` / Go
@@ -663,6 +664,16 @@ paths:
663664
schema:
664665
$ref: "#/components/schemas/Error"
665666
examples:
667+
missingSelector:
668+
summary: Required selector omitted
669+
value:
670+
code: INVALID_REQUEST
671+
message: "Invalid query criteria"
672+
details:
673+
error: "[INVALID_REQUEST] selector is required on /v1/query"
674+
requestId: "550e8400-e29b-41d4-a716-446655440000"
675+
timestamp: "2025-01-15T10:30:00Z"
676+
retryable: false
666677
invalidParameter:
667678
summary: Invalid query parameter
668679
value:

docs/user/api-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ All GET /v1/recipe parameters are supported, plus:
346346

347347
**Error Responses:**
348348

349+
Omitting `selector` returns `400 Bad Request` with code `INVALID_REQUEST`. An explicitly empty `selector=` remains valid and returns the entire hydrated recipe.
350+
349351
`GET /v1/query` and `POST /v1/query` resolve a recipe through the same engine as `/v1/recipe`, so a stated criteria dimension not honored by any applicable overlay fails the same way: `400 Bad Request` with the `details.uncovered` array described in the [POST /v1/recipe error responses](#post-v1recipe) above.
350352

351353
**Examples:**

pkg/server/recipe_handler.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,17 @@ func (h *recipeHandler) handleQuery(w http.ResponseWriter, r *http.Request, v2 b
406406
if err != nil {
407407
break
408408
}
409-
} else if _, supplied := r.URL.Query()[keyProfile]; supplied {
410-
err = aicrerrors.New(aicrerrors.ErrCodeInvalidRequest,
411-
"profile selection is available only on /v2/query")
412-
break
409+
} else {
410+
if _, supplied := r.URL.Query()[keyProfile]; supplied {
411+
err = aicrerrors.New(aicrerrors.ErrCodeInvalidRequest,
412+
"profile selection is available only on /v2/query")
413+
break
414+
}
415+
if !r.URL.Query().Has("selector") {
416+
err = aicrerrors.New(aicrerrors.ErrCodeInvalidRequest,
417+
"selector is required on /v1/query")
418+
break
419+
}
413420
}
414421
criteria, err = recipe.ParseCriteriaFromRequest(r, h.client.CriteriaRegistry())
415422
if !v2 {

pkg/server/recipe_handler_test.go

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,25 +1002,59 @@ func TestHandleQuery_SelectorNotFound(t *testing.T) {
10021002
}
10031003
}
10041004

1005-
// TestHandleQuery_NoSelector verifies a query with no selector returns the
1006-
// entire hydrated recipe structure (as the legacy handler does).
1007-
func TestHandleQuery_NoSelector(t *testing.T) {
1005+
// TestHandleQuery_SelectorPresence verifies an omitted selector is rejected
1006+
// while an explicitly empty selector returns the entire hydrated recipe.
1007+
func TestHandleQuery_SelectorPresence(t *testing.T) {
10081008
h := newTestHandler(t, nil)
10091009

1010-
req := httptest.NewRequest(http.MethodGet, "/v1/query?service=eks&accelerator=h100&intent=training", nil)
1011-
w := httptest.NewRecorder()
1010+
tests := []struct {
1011+
name string
1012+
target string
1013+
wantStatus int
1014+
wantError string
1015+
}{
1016+
{
1017+
name: "missing selector",
1018+
target: "/v1/query?service=eks&accelerator=h100&intent=training",
1019+
wantStatus: http.StatusBadRequest,
1020+
wantError: "[INVALID_REQUEST] selector is required on /v1/query",
1021+
},
1022+
{
1023+
name: "explicitly empty selector",
1024+
target: "/v1/query?service=eks&accelerator=h100&intent=training&selector=",
1025+
wantStatus: http.StatusOK,
1026+
},
1027+
}
10121028

1013-
h.HandleQuery(w, req)
1029+
for _, tt := range tests {
1030+
t.Run(tt.name, func(t *testing.T) {
1031+
req := httptest.NewRequest(http.MethodGet, tt.target, nil)
1032+
w := httptest.NewRecorder()
10141033

1015-
if w.Code != http.StatusOK {
1016-
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
1017-
}
1018-
var hydrated map[string]any
1019-
if err := json.Unmarshal(w.Body.Bytes(), &hydrated); err != nil {
1020-
t.Fatalf("failed to decode hydrated recipe: %v; body: %s", err, w.Body.String())
1021-
}
1022-
if _, ok := hydrated["components"]; !ok {
1023-
t.Errorf("expected hydrated recipe to contain a components key; got keys %v", keysOf(hydrated))
1034+
h.HandleQuery(w, req)
1035+
1036+
if w.Code != tt.wantStatus {
1037+
t.Fatalf("status = %d, want %d; body: %s", w.Code, tt.wantStatus, w.Body.String())
1038+
}
1039+
if tt.wantError != "" {
1040+
errResp := decodeErrorBody(t, w.Body.Bytes())
1041+
if errResp.Code != "INVALID_REQUEST" {
1042+
t.Errorf("code = %q, want INVALID_REQUEST", errResp.Code)
1043+
}
1044+
if got := errResp.Details[keyError]; got != tt.wantError {
1045+
t.Errorf("details.error = %q, want %q", got, tt.wantError)
1046+
}
1047+
return
1048+
}
1049+
1050+
var hydrated map[string]any
1051+
if err := json.Unmarshal(w.Body.Bytes(), &hydrated); err != nil {
1052+
t.Fatalf("failed to decode hydrated recipe: %v; body: %s", err, w.Body.String())
1053+
}
1054+
if _, ok := hydrated["components"]; !ok {
1055+
t.Errorf("expected hydrated recipe to contain a components key; got keys %v", keysOf(hydrated))
1056+
}
1057+
})
10241058
}
10251059
}
10261060

0 commit comments

Comments
 (0)