Skip to content

fix(client): drop content-type on any bodyless request, not just delete - #65

Open
sudiptpa wants to merge 2 commits into
sentdm:mainfrom
sudiptpa:fix-delete-empty-body
Open

sudiptpa wants to merge 2 commits into
sentdm:mainfrom
sudiptpa:fix-delete-empty-body

Conversation

@sudiptpa

@sudiptpa sudiptpa commented Oct 31, 2026 •

Copy link
Copy Markdown

What's wrong

webhooks->delete() sends no request body, but Client attaches its default
Content-Type: application/json header on every request, body or not. The API treats
that header as a promise of JSON content and rejects the empty body with a 400.

Confirmed live against api.sent.dm:

curl -X DELETE -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  https://api.sent.dm/v3/webhooks/{id} -d ""
→ 400 VALIDATION_001 "The value for 'body' is invalid or has the wrong type"

Every other delete-verb call in the SDK already sends an explicit body object, even when
empty, so none of them hit this: profiles, users, templates, contacts, profiles campaigns,
voice agents, and calls participants (both remove() and removeAll()). webhooks->delete()
is the one bodyless call in the whole SDK that isn't a GET.

Checked the other Sent SDKs before deciding where to fix this

Three sibling SDKs already solve this generically, at the request builder, not per method:

  • TypeScript (client.ts): "A resource method always passes a body key when its
    operation defines a request body, even if the caller omitted an optional body param.
    Keep the content-type for those, and only elide it for operations with no body at all
    (e.g. GET/DELETE)."
  • Ruby (base_client.rb): "Generated methods always pass req[:body] for operations
    that define a request body, so only elide the content-type header when the operation has
    no body at all, not when an optional body param was omitted."
  • Go (requestconfig.go): never sets Content-Type unless a request body was actually
    serialized (if reader != nil { req.Header.Set("Content-Type", contentType) }).
  • Java/Kotlin: no bug, same outcome by a different route. The generated body is only
    attached to the request when the params actually have one (params._body().ifPresent { ... }),
    and Content-Type is derived from that body object, so no body means no header.
  • C#: no bug either. BodyContent() defaults to null and WebhookDeleteParams never
    overrides it, so .NET's HttpContent-based Content-Type is naturally absent when there's
    no content object.
  • Python: likely has the same bug PHP had. is_body_allowed = options.method.lower() != "get" keeps Content-Type for any non-GET call regardless of whether a body was actually
    given, and none of Python's _base_client.py has the TS/Ruby/Go style "operation has no
    body at all" check. Not confirmed live, read from source only.

This PR fixes it at the same layer as TypeScript, Ruby, and Go: BaseClient::buildRequest().

What changed

The fix lives in BaseClient::buildRequest(): drop Content-Type from the merged headers
whenever the request has no body and the caller didn't set Content-Type explicitly.

Before:

webhooks->delete($id)
→ DELETE, body: (none), Content-Type: application/json
→ 400 VALIDATION_001

After:

webhooks->delete($id)
→ DELETE, body: (none), no Content-Type header
→ 204, deleted

An explicit override still wins: passing Content-Type in a call's own headers is
respected either way, same as before.

This also changes every GET call, on purpose

A GET request never has a body either, so the same condition that fixes the one DELETE
call also drops Content-Type from every typed GET method already in the SDK. Checked
every client->request() call site across every service: the only calls that omit the
body argument are the 29 GET methods and this one DELETE method. No POST, PUT, or PATCH
call omits it, so nothing else is affected.

Flagging this explicitly since it is a real behavior change, not just a bug fix for one
method: every GET request's wire format changes, even though no GET request had a body to
describe in the first place. This matches what TypeScript, Ruby, and Go already do for GET
too, not something new introduced here.

How I verified it

Added BaseClientTest-level coverage in ClientTest.php:

  • testBodylessDeleteOmitsContentType, confirmed it fails against the code before this
    change and passes after
  • testBodylessGetOmitsContentType, same before/after check for a GET call
  • testDeleteWithExplicitBodyKeepsContentType, confirms a delete that does send a body
    (e.g. profiles->delete()) is untouched
  • testBodylessRequestKeepsAnExplicitContentTypeOverride, confirms a caller-supplied
    Content-Type header still wins

./scripts/lint and ./scripts/test both pass.

Also re-verified the static claim above directly: counted every client->request() call
site across every service file by hand (not sampled), 67 total, 30 omit body: (29 GET,
this one DELETE), the other 37 (8 delete, 7 patch, 19 post, 3 put) all pass an explicit
body and are structurally untouched.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-31T08:04:56.923961Z
We couldn’t complete this review. Comment @codex review to try again.
f6daf50 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

webhooks->delete() sends no request body, but Client still attaches its
default Content-Type: application/json header. The API treats that header
as a promise of JSON content and rejects the empty body with a 400.

Every other delete-verb call in the SDK already sends an explicit body
object, even when empty, so none of them hit this: profiles, users,
templates, contacts, profiles campaigns, voice agents, and calls
participants (both remove() and removeAll()). webhooks->delete() is the
one bodyless delete in the SDK today.

Drop the header when no body is sent, instead of sending a body just to
get past the check.
@sudiptpa
sudiptpa force-pushed the fix-delete-empty-body branch from 2868586 to 89036c8 Compare October 31, 2026 06:20
@sudiptpa

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sudiptpa

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The per-method fix only covered webhooks->delete(). The real cause is
in BaseClient::buildRequest(): it attaches a default Content-Type
header to every request regardless of whether that request has a
body, which is what made webhooks->delete() 400 in the first place.

Move the fix there instead: drop Content-Type whenever a request has
no body and the caller didn't set one explicitly. This covers every
current and future bodyless operation, not just this one method, and
matches how the TypeScript and Ruby SDKs already handle the identical
case (confirmed by reading their request-building code directly).

This also changes every typed GET call, which never sends a body
either: GET requests no longer carry a Content-Type header. No GET
request has a body to describe, so this brings GET in line with the
same sibling SDKs' behavior rather than carrying a header that never
meant anything on those calls.
@sudiptpa sudiptpa changed the title fix(client): drop content-type header on bodyless delete requests fix(client): drop content-type on any bodyless request, not just delete Oct 31, 2026
@sudiptpa

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant