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 415

That 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.

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.

Edited by ANBUCHELVAN GANESAN CSE

Merge request reports

Loading
Loading