feat(api): support HEAD request bodies in OpenAPI client

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 16:37:13 +00:00
parent 19838779ef
commit b9f91811d8
2 changed files with 74 additions and 4 deletions

View file

@ -86,8 +86,8 @@ func NewOpenAPIClient(opts ...OpenAPIClientOption) *OpenAPIClient {
//
// The params argument may be a map, struct, or nil. For convenience, a map may
// include "path", "query", and "body" keys to explicitly control where the
// values are sent. When no explicit body is provided, non-GET requests send the
// remaining parameters as JSON.
// values are sent. When no explicit body is provided, requests with a declared
// requestBody send the remaining parameters as JSON.
func (c *OpenAPIClient) Call(operationID string, params any) (any, error) {
if err := c.load(); err != nil {
return nil, err
@ -273,7 +273,7 @@ func (c *OpenAPIClient) buildURL(op openAPIOperation, params map[string]any) (st
query.Set(key, fmt.Sprint(value))
}
}
if op.method == http.MethodGet || op.method == http.MethodHead {
if op.method == http.MethodGet || (op.method == http.MethodHead && !op.hasRequestBody) {
for key, value := range params {
if key == "path" || key == "body" || key == "query" {
continue
@ -300,7 +300,7 @@ func (c *OpenAPIClient) buildBody(op openAPIOperation, params map[string]any) (i
return encodeJSONBody(explicitBody)
}
if op.method == http.MethodGet || op.method == http.MethodHead {
if op.method == http.MethodGet || (op.method == http.MethodHead && !op.hasRequestBody) {
return nil, nil
}

View file

@ -4,6 +4,7 @@ package api_test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
@ -124,6 +125,75 @@ paths:
}
}
func TestOpenAPIClient_Good_CallHeadOperationWithRequestBody(t *testing.T) {
errCh := make(chan error, 1)
mux := http.NewServeMux()
mux.HandleFunc("/head", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
errCh <- fmt.Errorf("expected HEAD, got %s", r.Method)
w.WriteHeader(http.StatusInternalServerError)
return
}
if got := r.URL.RawQuery; got != "" {
errCh <- fmt.Errorf("expected no query string, got %q", got)
w.WriteHeader(http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
errCh <- fmt.Errorf("read body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if string(body) != `{"name":"Ada"}` {
errCh <- fmt.Errorf("expected JSON body {\"name\":\"Ada\"}, got %q", string(body))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
})
srv := httptest.NewServer(mux)
defer srv.Close()
specPath := writeTempSpec(t, `openapi: 3.1.0
info:
title: Test API
version: 1.0.0
paths:
/head:
head:
operationId: head_check
requestBody:
required: true
content:
application/json:
schema:
type: object
`)
client := api.NewOpenAPIClient(
api.WithSpec(specPath),
api.WithBaseURL(srv.URL),
)
result, err := client.Call("head_check", map[string]any{
"name": "Ada",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
select {
case err := <-errCh:
t.Fatal(err)
default:
}
if result != nil {
t.Fatalf("expected nil result for empty HEAD response body, got %T", result)
}
}
func TestOpenAPIClient_Bad_MissingOperation(t *testing.T) {
specPath := writeTempSpec(t, `openapi: 3.1.0
info: