# github-rest: verified behavior

Spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json
Version: 1.1.4 (spec hash 265c7bd16984), discovered 2026-10-11
Verified claims: 279 from run #59. Every claim below reproduced when re-run; numbers in brackets are the trial ids that back it.

## Where behavior contradicts the spec (12)

- **One pattern, 4 claims** Documented enums are not enforced: "direction", "order", "sort" accept values outside their enum with HTTP 200. On 2 endpoints: GET /repos/{owner}/{repo}/issues, GET /search/repositories. Spec says: enum of 2 values; enum of 3 values; enum of 4 values. [#7292, #7323, #7291, #7320, #7399, #7409, #7398, #7406]

- **One pattern, 8 claims** Declared types are not enforced: "page", "per_page" accept a wrong-type value ("abc") with HTTP 200. On 4 endpoints: GET /repos/{owner}/{repo}/commits, GET /repos/{owner}/{repo}/issues, GET /repos/{owner}/{repo}/releases, GET /search/repositories. Spec says: type: integer. [#7225, #7261, #7224, #7252, #7295, #7342, #7294, #7333, +8 more]

## Where this tool differs from sibling tools (0)

No sibling disagreements recorded.

## Rules the spec never states (1)

- **GET /repos/{owner}/{repo}/issues** On GET /repos/{owner}/{repo}/issues, the free-form string filters "milestone", "assignee" and "type" accept the wildcard "*" with HTTP 200, but arbitrary long strings are rejected with HTTP 422 "Validation Failed". [#7283, #7285, #7286, #7282, #7298, #7304, #7307]

## Behavior common to all 7 endpoints
- Responses set Cache-Control "no-cache". [#7197, #7217, #7282, #7363, #7204, #7396, #7436]
- Responses expose rate-limit headers (x-ratelimit-used, x-ratelimit-limit, x-ratelimit-reset, x-ratelimit-remaining); e.g. x-ratelimit-limit=5000. Read them to pace requests. [#7197, #7217, #7282, #7363, #7204, #7396, #7436]
- Timestamps in successful responses are ISO 8601 with a Z suffix (e.g. 2026-10-10T04:40:59Z). Times are UTC. (every endpoint except GET /rate_limit) [#7217, #7282, #7363, #7204, #7396, #7436]
- An invalid credential in "authorization" is rejected with HTTP 401 "Bad credentials". [#7197, #7199, #7217, #7277, #7282, #7358, #7363, #7391, +6 more]
- Omitting "authorization" entirely is accepted (HTTP 200), served as a different rate-limit tier (x-ratelimit-limit 60 vs 5000 on the baseline), i.e. unauthenticated. [#7197, #7200, #7217, #7278, #7282, #7359, #7363, #7392, +6 more]
- Unknown query parameters are ignored (HTTP 200). [#7197, #7198, #7217, #7276, #7282, #7357, #7363, #7390, +6 more]
- HEAD with the baseline query returns HTTP 200. [#7197, #7203, #7217, #7281, #7282, #7362, #7363, #7395, +6 more]
- The path with a trailing slash returns HTTP 404 "Not Found". (every endpoint except GET /repos/{owner}/{repo}/commits) [#7197, #7202, #7282, #7361, #7363, #7394, #7204, #7215, +4 more]
- Requesting XML via Accept/Content-Type headers yields HTTP 415 with content-type "application/json; charset=utf-8". [#7197, #7201, #7217, #7279, #7282, #7360, #7363, #7393, +6 more]
- A request with only the required parameters returns HTTP 200 JSON. [#7197, #7217, #7282, #7363, #7204, #7396, #7436]

## Endpoint behavior

### GET /rate_limit

Responses:
- The HTTP 200 response has a top-level "rate" object plus a "resources" object keyed by category (core, search, graphql, code_search, etc.), each holding used/limit/reset/remaining, where reset is a Unix epoch-seconds integer and the top-level "rate" mirrors "core" and the x-ratelimit-* headers. [#7197]

### GET /repos/{owner}/{repo}

Responses:
- In a successful GET /repos/{owner}/{repo} response (HTTP 200), "owner" is a nested account object with id, login, type (e.g. "Organization") and URL fields, rather than a plain string. [#7204]
- The baseline response includes 1 null-valued field(s) rather than omitting them (e.g. language). [#7204]

General:
- Validation errors consistently return HTTP 404 with a body of shape {"documentation_url":"string","message":"string","status":"string"} (6 of 6 rejected probes). [#7205, #7206, #7207, #7208, #7209, #7210]

Request validation:
- `owner` rejects a 2048-character string, an empty value, the literal "null" (HTTP 404) [#7204, #7207, #7206, #7205].
- `repo` rejects a 2048-character string, an empty value, the literal "null" (HTTP 404) [#7204, #7210, #7209, #7208].

### GET /repos/{owner}/{repo}/commits

Responses:
- Each GET /repos/{owner}/{repo}/commits item (HTTP 200) has top-level sha and url, a top-level "author" that is a GitHub account object (login, id, type such as "Bot"), and a nested "commit" object. [#7217]
- The baseline response includes 3 null-valued field(s) rather than omitting them (e.g. [2].commit.verification.payload, [2].commit.verification.signature, [2].commit.verification.verified_at). [#7217]

General:
- Paginated responses carry a Link header with rel=next, last; follow it rather than computing page numbers. [#7217]
- Validation errors consistently return HTTP 404 with a body of shape {"documentation_url":"string","message":"string","status":"string"} (8 of 8 rejected probes). [#7226, #7228, #7270, #7271, #7272, #7273, #7274, #7275]
- The path with a trailing slash returns HTTP 422 "No commit found for SHA:" (without it: HTTP 200). [#7217, #7280]

Request validation:
- `owner`, `repo` reject every invalid value tried with HTTP 404. [#7217, #7272, #7271, #7270, #7275, #7274, #7273]
- `author`, `committer`, `page`, `path`, `per_page`, `since`, `until` accept every invalid value tried without an error. [#7220, #7234, #7233, #7232, #7221, #7237, #7236, #7235, +30 more]
- `sha` rejects a 2048-character string, the literal "null" (HTTP 404) [#7218, #7228, #7226]; but accepts an empty value (HTTP 200) [#7218, #7227].
- `page`: A page beyond the last ("page"=999999999) returns HTTP 200 with an empty list rather than an error. [#7225, #7269]
- `per_page`: "per_page"=100000 returns 100 item(s): values above 100 are clamped rather than rejected. [#7224, #7259]
- `since`: Date parameter "since" accepts a non-date string ("not-a-date") with HTTP 200 but returns an EMPTY list, where the same request with "since"=2026-01-01T00:00:00Z returned 30 item(s): the invalid value is silently applied as a filter that matches nothing, with no error to signal bad input. Validate "since" before sending it. [#7222, #7244]
- `since`: Date parameter "since" accepts a date without a time (2026-01-15) (HTTP 200) and returns a full page of 30 item(s), as does "since"=2026-01-01T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7222, #7241]
- `since`: Date parameter "since" accepts a Unix epoch value (1768478400) (HTTP 200) and returns a full page of 30 item(s), as does "since"=2026-01-01T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7222, #7243]
- `since`: Date parameter "since" accepts an ISO date-time without a timezone designator (2026-01-15T12:00:00) (HTTP 200) and returns a full page of 30 item(s), as does "since"=2026-01-01T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7222, #7242]
- `until`: Date parameter "until" accepts a date without a time (2026-01-15) (HTTP 200) and returns a full page of 30 item(s), as does "until"=2026-12-31T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7223, #7248]
- `until`: Date parameter "until" accepts a Unix epoch value (1768478400) (HTTP 200) and returns a full page of 30 item(s), as does "until"=2026-12-31T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7223, #7250]
- `until`: Date parameter "until" accepts a non-date string ("not-a-date") (HTTP 200) and returns a full page of 30 item(s), as does "until"=2026-12-31T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7223, #7251]
- `until`: Date parameter "until" accepts an ISO date-time without a timezone designator (2026-01-15T12:00:00) (HTTP 200) and returns a full page of 30 item(s), as does "until"=2026-12-31T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7223, #7249]

### GET /repos/{owner}/{repo}/issues

Responses:
- The issues list (HTTP 200) returns a raw array with no pagination envelope, and each issue includes a "user" account object and a nullable "type" field. [#7282]
- The baseline response includes at least 5 null-valued field(s) rather than omitting them (e.g. [0].type, [0].assignee, [0].closed_at, [0].closed_by). [#7282]

General:
- Paginated responses carry a Link header with rel=next; follow it rather than computing page numbers. [#7282]
- Validation errors do not share one shape: 3 distinct status/shape combinations across 28 rejected probes (statuses 404, 422). [#7289, #7296, #7297, #7298, #7299, #7300, #7301, #7303]

Request validation:
- `milestone`, `owner`, `repo`, `since`, `state`, `type` reject every invalid value tried with HTTP 422/HTTP 404. [#7283, #7298, #7297, #7296, #7282, #7353, #7352, #7351, +15 more]
- `creator`, `direction`, `labels`, `mentioned`, `per_page`, `sort` accept every invalid value tried without an error. [#7287, #7310, #7309, #7308, #7292, #7325, #7324, #7290, +19 more]
- `assignee` rejects a 2048-character string, an empty value (HTTP 422) [#7285, #7304, #7303]; but accepts the literal "null" (HTTP 200) [#7285, #7302].
- `page` rejects 999999999, 100000, 2147483648 (HTTP 422) [#7295, #7350, #7349, #7347]; but accepts 0, -1, an empty value, the literal "null" (HTTP 200) [#7295, #7348, #7346, #7345, #7344, #7343].
- `issue_field_values`: On GET /repos/{owner}/{repo}/issues, issue_field_values=test is rejected with HTTP 422 "Validation Failed", while the baseline without it returns HTTP 200. [#7289, #7282]
- `issue_field_values`: An empty issue_field_values value is accepted with HTTP 200 even though the non-empty value "test" is rejected with HTTP 422. [#7315, #7289]
- `issue_field_values`: For issue_field_values, an empty value is accepted with HTTP 200, while the literal "null" and a 2048-character string are rejected with HTTP 422 "Validation Failed", the same as the plain value test. [#7315, #7314, #7316, #7289]
- `per_page`: "per_page"=100000 returns 100 item(s): values above 100 are clamped rather than rejected. [#7294, #7340]
- `since`: Date parameter "since" accepts a date without a time (2026-01-15) (HTTP 200) and returns a full page of 30 item(s), as does "since"=2026-01-01T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7293, #7329]
- `since`: Date parameter "since" rejects a Unix epoch value (1768478400) with HTTP 422 "The since parameter needs to be in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ". [#7293, #7331]
- `since`: Date parameter "since" rejects a non-date string ("not-a-date") with HTTP 422 "The since parameter needs to be in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ". [#7293, #7332]
- `since`: Date parameter "since" accepts an ISO date-time without a timezone designator (2026-01-15T12:00:00) (HTTP 200) and returns a full page of 30 item(s), as does "since"=2026-01-01T00:00:00Z; whether the value is applied cannot be told from the result count, and no error signals it. [#7293, #7330]

### GET /repos/{owner}/{repo}/releases

Responses:
- GET /repos/{owner}/{repo}/releases items (HTTP 200) embed an "assets" array whose entries carry size in bytes, state, a nullable digest, and a nested "uploader" account object. [#7363]
- The baseline response includes 3 null-valued field(s) rather than omitting them (e.g. [0].assets[0].digest, [2].assets[0].label, [2].assets[0].digest). [#7363]

General:
- Paginated responses carry a Link header with rel=next, last; follow it rather than computing page numbers. [#7364]
- Validation errors do not share one shape: 2 distinct status/shape combinations across 9 rejected probes (statuses 404, 422). [#7380, #7382, #7383, #7384, #7385, #7386, #7387, #7388]

Request validation:
- `owner`, `repo` reject every invalid value tried with HTTP 404. [#7363, #7386, #7385, #7384, #7389, #7388, #7387]
- `per_page` accepts 999999999, 100000, 0, 2147483648, -1, an empty value, the literal "null" (HTTP 200) [#7364, #7374, #7373, #7372, #7371, #7370, #7369, #7368, +1 more].
- `page` rejects 999999999, 100000, 2147483648 (HTTP 422) [#7365, #7383, #7382, #7380]; but accepts 0, -1, an empty value, the literal "null" (HTTP 200) [#7365, #7381, #7379, #7378, #7377, #7376].

### GET /search/repositories

Responses:
- A successful HTTP 200 search response is an object with "items" (the result array), "total_count", and an "incomplete_results" flag, and its x-ratelimit-limit header is 30 (the search bucket) rather than the 5000 core quota. [#7396]
- The baseline response includes 1 null-valued field(s) rather than omitting them (e.g. items[0].language). [#7396]
- The baseline response carries pagination/metadata fields at the top level: total_count. [#7396]

General:
- Validation errors do not share one shape: 2 distinct status/shape combinations across 7 rejected probes (statuses 422). [#7397, #7403, #7404, #7405, #7426, #7428, #7429]

Request validation:
- `order`, `per_page`, `sort` accept every invalid value tried without an error. [#7399, #7411, #7410, #7400, #7420, #7419, #7418, #7417, +7 more]
- `page` rejects 999999999, 100000, 2147483648 (HTTP 422) [#7401, #7429, #7428, #7426]; but accepts 0, -1, an empty value, the literal "null" (HTTP 200) [#7401, #7427, #7425, #7424, #7423, #7422].
- `q` rejects a 2048-character string, the parameter twice, an empty value (HTTP 422) [#7396, #7404, #7405, #7403]; but accepts the literal "null" (HTTP 200) [#7396, #7402].
- `q`: Omitting required parameter "q" is rejected with HTTP 422 "Validation Failed". [#7396, #7397]

### GET /users/{username}

Responses:
- Successful repository and user responses (HTTP 200) include hypermedia URL fields, some of which are RFC 6570-style URI templates with placeholders such as "{/gist_id}" or "{/owner}{/repo}" that the caller must expand. [#7436, #7204]
- The baseline response includes 3 null-valued field(s) rather than omitting them (e.g. bio, hireable, twitter_username). [#7436]

Request validation:
- `username` rejects a 2048-character string, an empty value (HTTP 404) [#7436, #7439, #7438]; but accepts the literal "null" (HTTP 200) [#7436, #7437].

## Other endpoints covered (7)
- GET /rate_limit: 13 claims
- GET /repos/{owner}/{repo}: 21 claims
- GET /repos/{owner}/{repo}/commits: 67 claims, 2 contradict the spec
- GET /repos/{owner}/{repo}/issues: 78 claims, 4 contradict the spec, 1 undocumented rule(s)
- GET /repos/{owner}/{repo}/releases: 39 claims, 2 contradict the spec
- GET /search/repositories: 44 claims, 4 contradict the spec
- GET /users/{username}: 17 claims
Fetch one: get_tool_docs {"tool":"github-rest","path":"/rate_limit","full":true}

## What this compendium covers
- This API: 7 GET endpoint(s) probed (listed in this sheet). Any other path on this host is NOT covered; do not infer its behavior from these.
- Other APIs: 14 (air-quality-api.open-meteo.com, api.anthropic.com, api.binance.com, api.binance.us, api.frankfurter.dev, api.open-meteo.com, api.weather.gov, date.nager.at, marine-api.open-meteo.com, openlibrary.org, pokeapi.co, pypi.org, registry.npmjs.org). list_tools gives each with its hosts.
- Not probed anywhere yet: write endpoints (POST/PUT/DELETE), request bodies, auth flows, hosts not listed above.
- Fetch: get_tool_docs {tool, path?, brief?} for an endpoint; search_claims {query} for one question; get_tool_knowledge {tool, kind} for structured claims; get_divergences {tool}; request_target to ask for an uncovered public API (needs a key).