fix(api): send --input bodies as JSON unless a Content-Type is given
Description
glab api --input sent the request body with no Content-Type header at all. GitLab rejects
that during content-type negotiation, which happens before routing and authorization:
{"error":"The provided content-type '' is not supported."}
glab: HTTP 415That included the example printed by glab api --help itself.
Corrected after @phikai's review — an earlier version of this description said "every --input
request failed", which is too strong. His breakdown:
| Case | Before | After |
|---|---|---|
Body-parsing endpoints (/markdown, most of the API) |
415, always | works |
| Raw-body endpoints (generic package upload) | worked with no header | still works |
Caller already passes -H |
works | unchanged; the default only fills a gap |
Non-GitLab host via full URL, non-JSON body, no -H |
worked | now labelled JSON |
Row 4 is the one behaviour change, and --header is the escape hatch — now documented in the
command's long description, not only in the flag help.
Why it happened
The three body modes had drifted apart:
| Mode | Content-Type |
|---|---|
--form |
prepends its own multipart/form-data; boundary=… |
--field / --raw-field |
marshalled to JSON, reaches NewHTTPRequest with bodyIsJSON true |
--input |
reaches it with bodyIsJSON false, so no header is ever set |
NewHTTPRequest already has exactly the right logic:
if bodyIsJSON && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
}It applies the default only when the caller supplied none. So rather than add a second
header-defaulting path, this passes that flag through from the --input branch. An explicit
--header still wins, and the header cannot be sent twice.
--form and --input are mutually exclusive (MarkFlagsMutuallyExclusive), and --form always
supplies its own content type, so the raw-body case stays unambiguous.
The --input flag help now states the default, and make gen-docs output is committed to match.
Related Issues
Resolves #8517 (closed)
How has this been tested?
Against the real API
The issue's repro needs no token, because the 415 is returned ahead of authentication:
$ printf '{"text":"hello"}' | glab api --hostname gitlab.com -X POST --input - markdown| Build | Result |
|---|---|
main |
{"error":"The provided content-type '' is not supported."} — HTTP 415 |
| this branch | {"message":"401 Unauthorized"} — routed and authorized, so negotiation passed |
this branch, -H 'Content-Type: text/plain' |
The provided content-type **'text/plain'** is not supported — the server echoes back the caller's header, proving the default did not override it |
Authenticated, the same request now succeeds instead of 415:
$ printf '{"text":"**hi**","gfm":true}' | glab api -X POST --input - markdown
{"html":"<p ...><strong ...>hi</strong></p>"}And the shipped --help example now reaches routing (404 Project Not Found against a
nonexistent project) where main still returns 415.
Jobs run locally
| Job | Command | Result |
|---|---|---|
tests:unit |
CGO_ENABLED=1 make test-race, re-run after rebasing onto c4ec2c749 |
5113 tests, 9 skipped. The only 2 failures, Test_LocalConfigDir and Test_LocalConfigFile, come from running in a git worktree (they fail the same way on plain main there) and pass on this commit in a normal clone |
lint |
golangci-lint run in golangci/golangci-lint:v2.13.1-alpine (the job's own image) |
0 issues |
check_docs_update |
make gen-docs, then git status |
one line changed, committed here |
check_docs_markdown |
vale + markdownlint-cli2 in the job's own image |
0 errors, 0 issues in 302 files |
check_go_generated_code |
make generate, then git status |
no unexpected diff |
check_embed |
make check-embed |
pass |
check_args |
make check-args |
pass |
Mutation testing
Each new assertion was checked by breaking the fix and confirming the right tests fail:
| Mutation | Result |
|---|---|
drop bodyIsJSON = rawBodyIsJSON in http.go |
fails the two JSON-default tests; the override test still passes |
drop bodyIsJSON = true in the --input branch of run |
fails only Test_apiRun_inputFile/stdin and /from_file (re-run after rebasing onto c4ec2c749) |
make the default unconditional (drop the Header.Get(...) == "" guard) |
fails both override tests — so those assertions are not vacuous |
One existing assertion, assert.Empty(t, resp.Request.Header.Get("Content-Type")), pinned the
buggy behaviour and is updated. A new case asserts an explicit Content-Type survives, and that
exactly one is sent.