-
Notifications
You must be signed in to change notification settings - Fork 1
fix(functions): treat invoke 2xx as success regardless of body shape (VOL-766) #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "mime/multipart" | ||
|
|
||
| "github.com/google/uuid" | ||
|
|
@@ -112,21 +113,54 @@ func (c *Client) UpdateFunctionVisibility(ctx context.Context, projectID, functi | |
| } | ||
|
|
||
| // InvokeFunction invokes one function by ID. | ||
| // | ||
| // The invoke endpoint passes through whatever the target function handler | ||
| // returns, so unlike other endpoints its response body isn't guaranteed to | ||
| // be a JSON object matching the generated schema (it may be plain text, | ||
| // HTML, empty, or non-object JSON). The generated WithResponse client only | ||
| // populates JSON200/JSONDefault when the body is a JSON object, so this | ||
| // calls the raw client method and classifies success/failure on the HTTP | ||
| // status code directly, decoding the body leniently rather than erroring | ||
| // out on a genuine 2xx. | ||
| func (c *Client) InvokeFunction(ctx context.Context, functionID uuid.UUID, input FunctionInvokeInput) (*apiclient.FunctionInvocationResponse, error) { | ||
| body := apiclient.InvokeFunctionJSONRequestBody{} | ||
| if input.Payload != nil { | ||
| payload := input.Payload | ||
| body.Payload = &payload | ||
| } | ||
|
|
||
| resp, err := c.client.InvokeFunctionWithResponse(ctx, functionID, body) | ||
| httpResp, err := c.client.InvokeFunction(ctx, functionID, body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if resp.JSONDefault != nil && resp.StatusCode() >= 200 && resp.StatusCode() < 300 { | ||
| return resp.JSONDefault, nil | ||
| defer func() { _ = httpResp.Body.Close() }() | ||
| respBody, err := io.ReadAll(httpResp.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 { | ||
| return decodeInvocationResponseBody(respBody), nil | ||
| } | ||
| return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON401, resp.JSON403, resp.JSON404, resp.JSON429, resp.JSON503) | ||
| return nil, apiError(httpResp.StatusCode, respBody) | ||
| } | ||
|
|
||
| // decodeInvocationResponseBody interprets a successful invocation's raw body | ||
| // leniently: a JSON object is used as-is, any other JSON value or plain-text | ||
| // body is wrapped under a "body" key, and an empty body yields an empty map. | ||
| func decodeInvocationResponseBody(body []byte) *apiclient.FunctionInvocationResponse { | ||
| if len(body) == 0 { | ||
| return &apiclient.FunctionInvocationResponse{} | ||
| } | ||
| var asMap apiclient.FunctionInvocationResponse | ||
| if json.Unmarshal(body, &asMap) == nil { | ||
| return &asMap | ||
| } | ||
| var asValue any | ||
| if json.Unmarshal(body, &asValue) == nil { | ||
| return &apiclient.FunctionInvocationResponse{"body": asValue} | ||
|
Comment on lines
+159
to
+161
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Large non-object invoke bodies get fully expanded into interface{} In Before this PR, an array response on this success path errored out inside the generated client's map-typed unmarshal without that expansion, so this memory cost is newly reachable via the fix's own success path and has no size guard. This only bites when a deployed function handler returns a large non-object body, but since function handlers are arbitrary user code, that's a realistic condition worth a size cap or streaming decode rather than an unconditional full-value unmarshal. |
||
| } | ||
| return &apiclient.FunctionInvocationResponse{"body": string(body)} | ||
| } | ||
|
|
||
| // ListFunctionRuntimes returns the function runtime catalog. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P3] Literal JSON null invoke body silently returns a nil map, not {"body":null}
decodeInvocationResponseBody's doc comment (internal/api/functions.golines 148-150) states any non-object JSON value is wrapped under a"body"key. A bare JSONnullbody breaks that:json.Unmarshal(body, &asMap)(line 156) succeeds withasMapleftnil—nullis valid input for a Go map — so the function returns a pointer to anilFunctionInvocationResponseinstead of{"body": null}. This also differs from the explicit empty-body case (line 153), which returns a non-nil empty map. Verified locally: unmarshalingnullintomap[string]interface{}returnserr=nil, map=nil.Impact is low (no crash; JSON-encodes as
nullinstead of{}/{"body":null}) since a handler literally returning barenullas its whole body is rare, but it's a real, silent deviation from the documented contract worth an explicitnullcheck.