Repository navigation
unexpected end of JSON input when sending a HEAD request with gh api #9927
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't workinggh-apirelating to the gh api commandrelating to the gh api commandpriority-3Affects a small number of users or is largely cosmeticAffects a small number of users or is largely cosmetic
Description
Describe the bug
When using gh api to request the headers(-X HEAD) of a non-existent release, the command exits with an exit code of 1 and prints unexpected end of JSON input to stderr. The issue seems to only affect non-existent releases, as requesting a release that exists does not have the same issue.
Steps to reproduce the behavior
- Run
gh api -X HEAD "repos/cli/cli/releases/tags/nonexistent" ; echo $? - See exit code of 1
- Run
gh api -X HEAD "repos/cli/cli/releases/tags/v2.62.0" ; echo $? - See exit code of 0
Expected vs actual behavior
Both commands should exit with a code of 0.
Logs
$ gh api -X HEAD "repos/cli/cli/releases/tags/nonexistent" ; echo $?
* Request at 2024-11-16 08:50:11.4016835 +0000 CST m=+0.077060401
* Request to https://api.github.com/repos/cli/cli/releases/tags/nonexistent
> HEAD /repos/cli/cli/releases/tags/nonexistent HTTP/1.1
> Host: api.github.com
> Accept: */*
> Authorization: token ████████████████████
> Content-Type: application/json; charset=utf-8
> Time-Zone: UTC
> User-Agent: GitHub CLI 2.61.0
< HTTP/2.0 404 Not Found
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Length: 133
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Sat, 16 Nov 2024 08:50:12 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Accepted-Oauth-Scopes: repo
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Api-Version-Selected: 2022-11-28
< X-Github-Media-Type: github.v3; format=json
< X-Github-Request-Id: C84C:36F44B:1BC8637:1D38C20:67385CC4
< X-Oauth-Client-Id: ████████████████████
< X-Oauth-Scopes: gist, read:org, repo, workflow
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4998
< X-Ratelimit-Reset: 1731750391
< X-Ratelimit-Resource: core
< X-Ratelimit-Used: 2
< X-Xss-Protection: 0
* Request took 545.7425ms
unexpected end of JSON input
1
$ gh api -X HEAD "repos/cli/cli/releases/tags/v2.62.0" ; echo $?
* Request at 2024-11-16 08:50:15.5118526 +0000 CST m=+0.067254201
* Request to https://api.github.com/repos/cli/cli/releases/tags/v2.62.0
> HEAD /repos/cli/cli/releases/tags/v2.62.0 HTTP/1.1
> Host: api.github.com
> Accept: */*
> Authorization: token ████████████████████
> Content-Type: application/json; charset=utf-8
> Time-Zone: UTC
> User-Agent: GitHub CLI 2.61.0
< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Cache-Control: private, max-age=60, s-maxage=60
< Content-Length: 37358
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Sat, 16 Nov 2024 08:50:16 GMT
< Etag: "3ee4cd9b98ceb34bcc6d030cac0282e4367b29115cfa1e9a055661b2c29c1a7c"
< Last-Modified: Thu, 14 Nov 2024 16:12:31 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
< X-Accepted-Oauth-Scopes: repo
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Api-Version-Selected: 2022-11-28
< X-Github-Media-Type: github.v3; format=json
< X-Github-Request-Id: C84D:35C4BD:1B9215D:1D027A6:67385CC8
< X-Oauth-Client-Id: ████████████████████
< X-Oauth-Scopes: gist, read:org, repo, workflow
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4997
< X-Ratelimit-Reset: 1731750391
< X-Ratelimit-Resource: core
< X-Ratelimit-Used: 3
< X-Xss-Protection: 0
* Request took 536.3801ms
0
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workinggh-apirelating to the gh api commandrelating to the gh api commandpriority-3Affects a small number of users or is largely cosmeticAffects a small number of users or is largely cosmetic
Activity
andyfeller commentedon Nov 18, 2024
@Arthri : Could you go into greater detail why this shouldn't be an error?
The GitHub API documentation doesn't officially state how
HEADmethod works with various endpoints such as Get a tag endpoint, however it is returning 404 response code because this tag does not exist.As you can see from
pkg/api/rest_client.goincli/go-gh, all 4XX and 5XX responses from the API are treated as client or server side errors respectively:ghcan't determine from context whether a given endpoint and user's circumstances are expected or not. In most cases, working with a non-existing GitHub primitive is unintentional and thus an error; situations where people are attempting to discover information typically needing to implement special logic to avoid these response codes or exit codes to continue on.Anything you can do to elaborate on why
HEADrequests for non-existent primitives shouldn't be treated as an error including excerpts from other tools and what use cases you're attempt to solve would be helpful!Arthri commentedon Nov 19, 2024
This should be an error.
I apologize for the misunderstanding. In retrospect, both commands I've noted are working as expected. I understand now that using
gh apito read non-existing resources will cause the command to exit with a status code of 1 regardless of whether-X HEADis specified, which is the normal behaviorI use the API request to determine whether a tag exists without pulling all tags from the remote repository. In which case, I want it to fail specifically with a status code of 404. And, I realize now the paradoxical expectation I had in the issue
I suppose that I was confused by the message
unexpected end of JSON input, for two reasonsHEADrequests to an extent and terminated early instead of attempting to read an empty bodyThank you for your response and clarification. Should I close the issue? I believe that my issue is resolved, however, I wouldn't mind if you'd like to keep the issue open to track the last point I mentioned
andyfeller commentedon Nov 19, 2024
Yeah, that isn't a great experience 🤔 unsure what can be done to improve handling the error raised in this scenario. Pivoting to
curlto see what difference there is behind the responses.curl --verbose "https://api.github.com/repos/cli/cli/releases/tags/v2.62.0" -X HEADWarning: Setting custom HTTP method to HEAD with -X/--request may not work the Warning: way you want. Consider using -I/--head instead. % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Host api.github.com:443 was resolved. * IPv6: (none) * IPv4: 140.82.114.6 * Trying 140.82.114.6:443... * Connected to api.github.com (140.82.114.6) port 443 * ALPN: curl offers h2,http/1.1 * (304) (OUT), TLS handshake, Client hello (1): } [319 bytes data] * CAfile: /etc/ssl/cert.pem * CApath: none * (304) (IN), TLS handshake, Server hello (2): { [122 bytes data] * (304) (IN), TLS handshake, Unknown (8): { [19 bytes data] * (304) (IN), TLS handshake, Certificate (11): { [3136 bytes data] * (304) (IN), TLS handshake, CERT verify (15): { [78 bytes data] * (304) (IN), TLS handshake, Finished (20): { [36 bytes data] * (304) (OUT), TLS handshake, Finished (20): } [36 bytes data] * SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF * ALPN: server accepted h2 * Server certificate: * subject: CN=*.github.com * start date: Mar 7 00:00:00 2024 GMT * expire date: Mar 7 23:59:59 2025 GMT * subjectAltName: host "api.github.com" matched cert's "*.github.com" * issuer: C=GB; ST=Greater Manchester; L=Salford; O=Sectigo Limited; CN=Sectigo ECC Domain Validation Secure Server CA * SSL certificate verify ok. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://api.github.com/repos/cli/cli/releases/tags/v2.62.0 * [HTTP/2] [1] [:method: HEAD] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: api.github.com] * [HTTP/2] [1] [:path: /repos/cli/cli/releases/tags/v2.62.0] * [HTTP/2] [1] [user-agent: curl/8.7.1] * [HTTP/2] [1] [accept: */*] > HEAD /repos/cli/cli/releases/tags/v2.62.0 HTTP/2 > Host: api.github.com > User-Agent: curl/8.7.1 > Accept: */* > * Request completely sent off < HTTP/2 200 < date: Tue, 19 Nov 2024 20:40:49 GMT < content-type: application/json; charset=utf-8 < cache-control: public, max-age=60, s-maxage=60 < vary: Accept,Accept-Encoding, Accept, X-Requested-With < etag: W/"7141dcef2b4dcfa7c00ebdefd3a2d4056b52d22e2938364fc40ef11dac82ff4f" < last-modified: Thu, 14 Nov 2024 16:12:31 GMT < x-github-media-type: github.v3; format=json < x-github-api-version-selected: 2022-11-28 < access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset < access-control-allow-origin: * < strict-transport-security: max-age=31536000; includeSubdomains; preload < x-frame-options: deny < x-content-type-options: nosniff < x-xss-protection: 0 < referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin < content-security-policy: default-src 'none' < server: github.com < x-ratelimit-limit: 60 < x-ratelimit-remaining: 50 < x-ratelimit-reset: 1732051961 < x-ratelimit-resource: core < x-ratelimit-used: 10 < accept-ranges: bytes < content-length: 44168 < x-github-request-id: C8DD:34DB9A:E64EAB:1C1B7B9:673CF7D7 < { [0 bytes data] * transfer closed with 44168 bytes remaining to read 0 44168 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 * Connection #0 to host api.github.com left intact curl: (18) transfer closed with 44168 bytes remaining to readcurl --verbose "https://api.github.com/repos/cli/cli/releases/tags/nonexistent" -X HEADWarning: Setting custom HTTP method to HEAD with -X/--request may not work the Warning: way you want. Consider using -I/--head instead. % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Host api.github.com:443 was resolved. * IPv6: (none) * IPv4: 140.82.114.6 * Trying 140.82.114.6:443... * Connected to api.github.com (140.82.114.6) port 443 * ALPN: curl offers h2,http/1.1 * (304) (OUT), TLS handshake, Client hello (1): } [319 bytes data] * CAfile: /etc/ssl/cert.pem * CApath: none * (304) (IN), TLS handshake, Server hello (2): { [122 bytes data] * (304) (IN), TLS handshake, Unknown (8): { [19 bytes data] * (304) (IN), TLS handshake, Certificate (11): { [3136 bytes data] * (304) (IN), TLS handshake, CERT verify (15): { [79 bytes data] * (304) (IN), TLS handshake, Finished (20): { [36 bytes data] * (304) (OUT), TLS handshake, Finished (20): } [36 bytes data] * SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF * ALPN: server accepted h2 * Server certificate: * subject: CN=*.github.com * start date: Mar 7 00:00:00 2024 GMT * expire date: Mar 7 23:59:59 2025 GMT * subjectAltName: host "api.github.com" matched cert's "*.github.com" * issuer: C=GB; ST=Greater Manchester; L=Salford; O=Sectigo Limited; CN=Sectigo ECC Domain Validation Secure Server CA * SSL certificate verify ok. * using HTTP/2 * [HTTP/2] [1] OPENED stream for https://api.github.com/repos/cli/cli/releases/tags/nonexistent * [HTTP/2] [1] [:method: HEAD] * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: api.github.com] * [HTTP/2] [1] [:path: /repos/cli/cli/releases/tags/nonexistent] * [HTTP/2] [1] [user-agent: curl/8.7.1] * [HTTP/2] [1] [accept: */*] > HEAD /repos/cli/cli/releases/tags/nonexistent HTTP/2 > Host: api.github.com > User-Agent: curl/8.7.1 > Accept: */* > * Request completely sent off < HTTP/2 404 < date: Tue, 19 Nov 2024 20:40:49 GMT < content-type: application/json; charset=utf-8 < x-github-media-type: github.v3; format=json < x-github-api-version-selected: 2022-11-28 < access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset < access-control-allow-origin: * < strict-transport-security: max-age=31536000; includeSubdomains; preload < x-frame-options: deny < x-content-type-options: nosniff < x-xss-protection: 0 < referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin < content-security-policy: default-src 'none' < vary: Accept-Encoding, Accept, X-Requested-With < server: github.com < x-ratelimit-limit: 60 < x-ratelimit-remaining: 49 < x-ratelimit-reset: 1732051961 < x-ratelimit-resource: core < x-ratelimit-used: 11 < content-length: 147 < x-github-request-id: C8DE:22DBE9:D600CE:1A28693:673CF7D7 < { [0 bytes data] * transfer closed with 147 bytes remaining to read 0 147 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 * Connection #0 to host api.github.com left intact curl: (18) transfer closed with 147 bytes remaining to readLet me raise the question to other maintainers as I'm unsure what can be done here.
andyfeller commentedon Nov 19, 2024
Stepping through the debugger,
gh apicommands are relying upon different logic than I previously assumed for processing REST and GraphQL calls. While it does treat any 4XX or 5XX responses as errors, I think it assumes the JSON response contains content when it doesn't:cli/pkg/cmd/api/api.go
Lines 447 to 468 in 9177b22
cli/pkg/cmd/api/api.go
Lines 610 to 624 in 9177b22
In the following screenshot from debug session, you can see the
bis empty when reading the response from GitHub API:jsoref commentedon Feb 7, 2025
I tripped on this with:
There's no valid excuse to try to parse the body of a response to a HEAD request. The whole point of issuing a HEAD request is to tell the client and server not to provide a body.
jtmcg commentedon Feb 7, 2025
Nice catch, @jsoref, and an elegant fix.
That said, I can think of a valid excuse that we're trying to parse the body, here, and that's simply that this got overlooked before among the multitudes of priorities we juggle implementing and maintaining this project. You yourself as an open-source maintainer can surely empathize.
That's why we appreciate folks like you and @Arthri bringing things like this to our attention and finding the quirks like this that have missed our attention. I'd request that you are a bit more gracious when highlighting these types of discoveries in the future.
jtmcg commentedon Feb 7, 2025
Acceptance Criteria
Given I make an api call with the
-X HEADflagwhen the response status is
>= 400then the response body is not returned