Skip to content

PHP/Go/Ruby SDKs send Content-Type: application/json on bodyless requests; respectOptionalRequestBody does not cover endpoints without a request body #17499

Description

@bobsingor

Component

SDKs

Priority

P2 - Medium (Would be helpful)

SDK Language(s)

PHP, Go, Ruby

Summary

Endpoints with no request body at all (a bare DELETE, for example) still send Content-Type: application/json with an empty body in the PHP, Go, and Ruby SDKs. Strict servers reject that combination before the route handler runs — Fastify throws FST_ERR_CTP_EMPTY_JSON_BODY:

{
  "statusCode": 400,
  "code": "FST_ERR_CTP_EMPTY_JSON_BODY",
  "message": "Body cannot be empty when content-type is set to 'application/json'"
}

The respectOptionalRequestBody option (fern-php-sdk 2.20.0) fixes the adjacent case — an optional body the caller omits — but does not reach this one: generators/php/sdk/src/endpoint/utils/mayOmitRequestBody.ts requires requestBody?.type === "reference" && requestBody.required === false, so an endpoint with requestBody === undefined never gets omitContentTypeWithoutBody: true from HttpEndpointGenerator, and the default header still goes out even on 2.20.4 with the flag enabled.

Repro

OpenAPI operation: a DELETE with path parameters and no requestBody. Generated PHP client code:

$response = $this->client->sendRequest(
    new JsonApiRequest(
        baseUrl: $options['baseUrl'] ?? $this->client->options['baseUrl'] ?? '',
        path: "v1/tenants/{$tenantId}/documents/{$id}",
        method: HttpMethod::DELETE,
    ),
    $options,
);

RawClient::encodeHeaders() stamps Content-Type: application/json for every JsonApiRequest regardless of body; encodeRequestBody() correctly attaches nothing. On the wire:

DELETE /v1/tenants/local/documents/{id} HTTP/1.1
Content-Type: application/json

(no body)

Affected generators

  • PHPgenerators/php/base/src/asIs/Client/RawClient.Template.php: encodeHeaders() sets the default Content-Type unconditionally for JsonApiRequest/UrlEncodedApiRequest. The header-drop logic added for respectOptionalRequestBody is gated on the per-request $request->omitContentTypeWithoutBody, which is only emitted as true for optional referenced bodies.
  • Go — generated internal/caller.go: req.Header.Set(contentTypeHeader, reqContentType) runs even when newRequestBody(...) returned nil for a bodyless request.
  • Ruby — generated internal/json/request.rb: encode_headers always includes "Content-Type" => "application/json", while encode_body returns nil.

TypeScript, Python, Java, and C# already behave correctly (no body → no Content-Type).

Ask

When no body is attached to the outgoing request, don't emit the default Content-Type. For bodyless endpoints this involves no method-signature change, so it could arguably be unconditional (a wire-only correction); at minimum, extend respectOptionalRequestBody so mayOmitRequestBody also returns true when endpoint.requestBody is absent — in the PHP, Go, and Ruby generators alike.

A Content-Type explicitly supplied by the caller should keep going out, matching the behavior of the existing drop logic.

Versions

No response

Workaround

No response

Are you interested in contributing a fix?

No

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions