Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/explanation/wire-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ the [archived phase notes](../archive/phases/)):
| `POST /repos` with both `upstream` and `base_repo`: "do not set" both, no documented error | 400 | Ambiguous request; failing loudly beats guessing which form wins |
| `POST /repos` without `default_branch`: a sync from GitHub takes the GitHub repository's default branch (spec `40e43e00`, 2026-10-28) | PostgreSQL and SQLite: the repository starts on `main`, and the first successful sync adopts the upstream `HEAD` branch before it sends `repo.sync.succeeded`. S3-WAL: the repository keeps `main` | The create makes no network call ([#599](https://github.com/coder/openstorage/issues/599)). S3-WAL metadata v2 is frozen and cannot store the "branch omitted" flag ([#603](https://github.com/coder/openstorage/issues/603), trigger: the next S3-WAL metadata format change) |
| `POST /repos` is idempotent on the name: `200` for any existing name unless its upstream differs (spec 2026-10-07) | A finished repo answers `200` to a matching repeat create: a sync create with the same upstream and `forward_pushes`, or a plain, copy or fork create of a repo without an upstream. A different upstream is `409`, as upstream documents, and so is a create form that does not match the repo (sync onto a plain repo, plain onto a synced repo). Repos created before create tracking (on S3-WAL, every repo created by v0.18.0 or earlier) answer `409` to every repeat. While a copy or fork create is still running, plain, copy and fork creates of the name are `503` with `Retry-After`. A pending sync create is finished by a matching sync create with `git:write` (`200`); other forms are `409` ([Repeat creates](../reference/http-api.md#repository-lifecycle-repowrite--orgread)). A name a deleted repo holds is `404` | A `200` names a repo only after its create finished, so it never names a repo that the failing create then deletes ([#388](https://github.com/coder/openstorage/issues/388)). A form mismatch is `409` because a `200` would hide that the request was not applied. Older repos store no create state (#335 for S3-WAL) |
| RFC 9457 problem-details error bodies (spec). Since upstream's 2026-09-24 spec update, `ProblemDetails` requires a snake_case `code` and tells clients to branch on `code`, not on `detail` | The plain `{"error":"<msg>"}` envelope carries problem-details fields next to `error`: `type` (`about:blank`), `title`, `status`, `detail` (same string as `error`), `code` ([#349](https://github.com/coder/openstorage/issues/349)), and `instance`, the request path without the query ([#352](https://github.com/coder/openstorage/issues/352)). `code` is upstream's generic per-status code, except for nine of upstream's specific codes: `repository_not_found`, `insufficient_scope`, `repo_name_mismatch`, `branch_not_found`, `ref_not_found`, `credential_not_found`, `credential_exists`, `repository_deleted` ([#352](https://github.com/coder/openstorage/issues/352)), and `ref_guard_failed` with its `guard` and `current_sha` members on every failed ref guard ([#568](https://github.com/coder/openstorage/issues/568), [#647](https://github.com/coder/openstorage/issues/647)). A 400 for a request body carries the optional `invalid_params` list (`ValidationProblemDetails`, spec 2026-10-08, [#396](https://github.com/coder/openstorage/issues/396)) when open.storage can name the invalid fields: body decode failures (wrong JSON type, and unknown, duplicate or `null` members of strictly decoded bodies) and single-field checks (required fields, allowed values, ranges, name formats). Every body with the problem members has `Content-Type: application/problem+json`, as upstream documents and serves ([#352](https://github.com/coder/openstorage/issues/352) item 5); result envelopes and success bodies stay `application/json`. Not emitted: upstream's other specific codes (such as `route_not_found`) and `invalid_params` for failures that cannot name one field. The [streaming protocols](streaming-commit-protocols.md) `{"result":{"success":false,…}}` envelope carries no problem-details fields, except the 409 body of a failed ref guard on a commit write or a notes write, which keeps the `result` object (and the note members) and adds the problem members as upstream's `CommitWriteRefGuardProblemDetails`, `RestoreCommitRefGuardProblemDetails` and `NotesWriteRefGuardProblemDetails`, sent as `application/problem+json`. Auth failures (401, 403), the gateway 429 and the path-style alias and git-credentials failures use the plain problem body on every route, as upstream documents ([#352](https://github.com/coder/openstorage/issues/352) item 3). `commit-pack`, `diff-commit` and `restore-commit` send the envelope only on the statuses for which upstream documents `CommitWriteResponse` or `ReposCommitsRestoreResponse` (400, 409, 412 and 413 on commit-pack; 400, 409 and 412 on diff-commit; 412 on restore-commit) and the plain problem body on every other failure. Merge answers file conflicts with upstream's `MergeConflict` (`error`, `conflict_type: merge_conflict`, `conflict_paths`, `merge_base_sha`) and every other failure with the plain problem body. A target that moved during the merge is a 409 `conflict` problem body: upstream sends `MergeConflict` with `target_moved` or `repo_changed` only after an automatic stale-target retry, which open.storage does not do. Note writes answer every failure with the plain problem body except the guard body above. Failures use one of these shapes, by route and failure class. Do not assume one shape per route family: branch on the status code, or check both shapes. Per-route contracts are in [HTTP API](../reference/http-api.md) and [streaming protocols](streaming-commit-protocols.md) | Recorded deviations ([phase-3 notes](../archive/phases/12-phase3-implementation-notes.md), [ADR-26](../adr/026-result-envelope-statuses-499-is-not-emittable.md)): all three SDKs branch on status codes. At the 2026-09-25 audit this held for SDK `1.17.1` (stable) and `1.18.0-beta.0` (beta): no SDK reads `code`, `detail`, or any problem-details field other than the legacy `error` string. SDK `0.1.0` (Go, Python, TypeScript, stable since 2026-11-04) reads every error body as JSON whatever its type, and the `1.18.0-beta` clients accept `application/problem+json`; the `1.17.1` Go and TypeScript clients parse error JSON only for `application/json`, so on a problem body they report the raw body text as the message (a breaking change for them, accepted because upstream sends the same type). Upstream says to treat an unknown code as the generic code for its status, so a client that follows that guidance handles every `code` open.storage sends; a client that expects one of the specific codes open.storage does not emit sees the generic one instead (for example `not_found` for `route_not_found`). `status` is a JSON number, as in upstream's bodies: a client that decoded the old `{"error"}` body into a string-only map (such as Go's `map[string]string`) must switch to a struct or `map[string]any`. At the 2026-10-08 audit, no SDK (`1.17.1`, `1.18.0-beta.1`) reads `invalid_params`; the member is optional, so a client that ignores it sees no change |
| RFC 9457 problem-details error bodies (spec). Since upstream's 2026-09-24 spec update, `ProblemDetails` requires a snake_case `code` and tells clients to branch on `code`, not on `detail` | The plain `{"error":"<msg>"}` envelope carries problem-details fields next to `error`: `type` (`about:blank`), `title`, `status`, `detail` (same string as `error`), `code` ([#349](https://github.com/coder/openstorage/issues/349)), and `instance`, the request path without the query ([#352](https://github.com/coder/openstorage/issues/352)). `code` is upstream's generic per-status code, except for eleven of upstream's specific codes: `repository_not_found`, `insufficient_scope`, `repo_name_mismatch`, `branch_not_found`, `ref_not_found`, `credential_not_found`, `credential_exists`, `repository_deleted`, `authentication_unavailable` (403, key store failure) and `route_not_found` (404, with a 405 `method_not_allowed` problem body for a path that has routes for other methods) ([#352](https://github.com/coder/openstorage/issues/352)), and `ref_guard_failed` with its `guard` and `current_sha` members on every failed ref guard ([#568](https://github.com/coder/openstorage/issues/568), [#647](https://github.com/coder/openstorage/issues/647)). A 400 for a request body carries the optional `invalid_params` list (`ValidationProblemDetails`, spec 2026-10-08, [#396](https://github.com/coder/openstorage/issues/396)) when open.storage can name the invalid fields: body decode failures (wrong JSON type, and unknown, duplicate or `null` members of strictly decoded bodies) and single-field checks (required fields, allowed values, ranges, name formats). Every body with the problem members has `Content-Type: application/problem+json`, as upstream documents and serves ([#352](https://github.com/coder/openstorage/issues/352) item 5); result envelopes and success bodies stay `application/json`. Not emitted: upstream's other specific codes and `invalid_params` for failures that cannot name one field. `repo_claim_missing` is not sent because a token without a `repo` claim is org-wide ([#658](https://github.com/coder/openstorage/issues/658)): a per-repo route refuses it with 403, as it refuses `repo: "org"`. `merge_conflict` is listed upstream, but the merge 409 for file conflicts is documented as `MergeConflict`, which has no `code` member; `sync_in_progress` and `upstream_already_configured` are documented on no operation, and no open.storage answer matches them; the hosting, deployment, GitHub App, cold-storage and idempotency codes belong to features open.storage lacks. The [streaming protocols](streaming-commit-protocols.md) `{"result":{"success":false,…}}` envelope carries no problem-details fields, except the 409 body of a failed ref guard on a commit write or a notes write, which keeps the `result` object (and the note members) and adds the problem members as upstream's `CommitWriteRefGuardProblemDetails`, `RestoreCommitRefGuardProblemDetails` and `NotesWriteRefGuardProblemDetails`, sent as `application/problem+json`. Auth failures (401, 403), the gateway 429 and the path-style alias and git-credentials failures use the plain problem body on every route, as upstream documents ([#352](https://github.com/coder/openstorage/issues/352) item 3). `commit-pack`, `diff-commit` and `restore-commit` send the envelope only on the statuses for which upstream documents `CommitWriteResponse` or `ReposCommitsRestoreResponse` (400, 409, 412 and 413 on commit-pack; 400, 409 and 412 on diff-commit; 412 on restore-commit) and the plain problem body on every other failure. Merge answers file conflicts with upstream's `MergeConflict` (`error`, `conflict_type: merge_conflict`, `conflict_paths`, `merge_base_sha`) and every other failure with the plain problem body. A target that moved during the merge is a 409 `conflict` problem body: upstream sends `MergeConflict` with `target_moved` or `repo_changed` only after an automatic stale-target retry, which open.storage does not do. Note writes answer every failure with the plain problem body except the guard body above. Failures use one of these shapes, by route and failure class. Do not assume one shape per route family: branch on the status code, or check both shapes. Per-route contracts are in [HTTP API](../reference/http-api.md) and [streaming protocols](streaming-commit-protocols.md) | Recorded deviations ([phase-3 notes](../archive/phases/12-phase3-implementation-notes.md), [ADR-26](../adr/026-result-envelope-statuses-499-is-not-emittable.md)): all three SDKs branch on status codes. At the 2026-09-25 audit this held for SDK `1.17.1` (stable) and `1.18.0-beta.0` (beta): no SDK reads `code`, `detail`, or any problem-details field other than the legacy `error` string. SDK `0.1.0` (Go, Python, TypeScript, stable since 2026-11-04) reads every error body as JSON whatever its type, and the `1.18.0-beta` clients accept `application/problem+json`; the `1.17.1` Go and TypeScript clients parse error JSON only for `application/json`, so on a problem body they report the raw body text as the message (a breaking change for them, accepted because upstream sends the same type). Upstream says to treat an unknown code as the generic code for its status, so a client that follows that guidance handles every `code` open.storage sends; a client that expects one of the specific codes open.storage does not emit sees the generic one instead. `status` is a JSON number, as in upstream's bodies: a client that decoded the old `{"error"}` body into a string-only map (such as Go's `map[string]string`) must switch to a struct or `map[string]any`. At the 2026-10-08 audit, no SDK (`1.17.1`, `1.18.0-beta.1`) reads `invalid_params`; the member is optional, so a client that ignores it sees no change |
| `201` on write endpoints (spec) | `commit-pack` / `diff-commit` / `restore-commit` and `POST`/`DELETE .../notes` return `200`. The path-style `POST .../notes/append` returns `201` | Recorded deviation ([phase-3 notes](../archive/phases/12-phase3-implementation-notes.md)): the SDKs accept both; schema-validating clients should expect `200` on those legacy routes and `201` on `notes/append` |
| `restore-commit`: `412` when the branch already points at the target, the tree would not change, or the target is not an ancestor of the tip (spec) | Enforced for `base_ref`. The deprecated `target_commit_sha` form keeps restoring without these checks | The SDKs send `target_commit_sha`; adding the checks there would change what SDK callers see |
| `DELETE .../branches` naming the default branch: `409` (spec) | `409` for the `target_branch` query parameter form. `403` for the deprecated body form | The SDKs send the body form; changing its status would change what SDK callers see |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ branch-protection mechanism; there is no server-side protection config.
| Condition | Status |
|---|---|
| missing/invalid/expired token | 401 |
| the key store fails while the server verifies the token | REST API, fork source tokens included: 403 `authentication_unavailable` (retry). Git and LFS transports: 401, as for an invalid token |
| valid token, insufficient scope or ref policy denies | 403 |
| `repo` claim mismatch with target | 403 (do not leak existence: 404 acceptable per vendor error-string instability) |

Expand Down
20 changes: 13 additions & 7 deletions docs/reference/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ are in [`../upstream/openapi.yaml`](../upstream/openapi.yaml).
revision does not resolve), `credential_not_found` (404), `credential_exists` (409),
`repository_deleted` (409: `DELETE /api/v1/repos/delete` on a deleted repository;
`DELETE /api/repos/{repo_name}` answers that case with 404 `repository_not_found`,
because its name lookup skips deleted repositories), and `ref_guard_failed` (409: a
because its name lookup skips deleted repositories), `ref_guard_failed` (409: a
failed ref guard, with `guard` and `current_sha`: on a merge, [details](#merge-guards),
on a commit write, [details](#commit-write-guards), and on a notes write,
[details](#notes)). open.storage
emits no other specific codes. As upstream advises, treat
an unknown code as the generic code for its status. Branch on the status code or
`code`, not on the message.
[details](#notes)), `authentication_unavailable` (403: the key store failed while
verifying the token; retry), and `route_not_found` (404: no API route matches the
path; a path that has routes for other methods answers 405 `method_not_allowed` with
`Allow`). open.storage emits no other specific codes. It does not send
`repo_claim_missing`: a token without a `repo` claim is org-wide, so a per-repo route
refuses it with 403 `forbidden`, as it refuses `repo: "org"`
([authentication](authentication.md)). As upstream advises, treat an unknown code as
the generic code for its status. Branch on the status code or `code`, not on the
message.
A `400` for a request body can also carry `invalid_params` (`[{name, reason}]`, as
upstream's `ValidationProblemDetails`): `name` is the dotted wire path of the invalid
field, and `reason` is the rule its value breaks. open.storage sends it when a body
Expand Down Expand Up @@ -682,8 +687,9 @@ Other limits and gaps:
| 202 | pull-upstream / push-upstream accepted |
| 206/304/412/416 | file conditional/range results (passed through) |
| 400 | invalid input; also "custom notes refs not enabled" |
| 401 / 403 | auth failed / permission or ref-policy denied |
| 404 | repo/object/branch not found; unborn-branch file metadata |
| 401 / 403 | auth failed / permission or ref-policy denied; 403 `authentication_unavailable` when the key store fails (retry) |
| 404 | repo/object/branch not found; unborn-branch file metadata; no API route matches the path (`route_not_found`) |
| 405 | the path has routes, but not for this method (`Allow` lists them) |
| 409 | repo exists, repo already deleted, credential exists, merge conflict, failed ref guard (`expected_target_sha`, `expected_source_sha`, `expected_notes_ref_sha`), CAS conflict, a `diff-commit` patch that does not apply to the base ([streaming protocols](../explanation/streaming-commit-protocols.md)) |
| 412 | a failed `restore-commit` `base_ref` precondition; a merge that is not a fast-forward under `ff_only`, or of unrelated histories without `allow_unrelated_histories`; a `commit-pack` or `diff-commit` that changes nothing (`no changes to commit`) or whose stream ends before an `eof` chunk ([streaming protocols](../explanation/streaming-commit-protocols.md)) |
| 413 | a `commit-pack` whose decoded file contents pass 262144000 bytes ([streaming protocols](../explanation/streaming-commit-protocols.md)) |
Expand Down
2 changes: 1 addition & 1 deletion internal/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The REST API (`/api/v1/...`) and its `/api/repos/{repo_name}` alias layer. `New`

## Invariants
- `New` panics when a required dependency is missing and returns an error for a bad cache dir or rate-limit config [api.go:New]
- `requireAuth` returns 401 for a missing or invalid bearer token and 403 for a wrong org, scope, or repo claim [middleware.go:requireAuth]
- `requireAuth` returns 401 for a missing or invalid bearer token, 403 `authentication_unavailable` when the key store fails, and 403 for a wrong org, scope, or repo claim [middleware.go:requireAuth]
- `recoverPanics` re-panics `http.ErrAbortHandler` and otherwise answers 500 [middleware.go:recoverPanics]
- `newProblem` asserts a 4xx/5xx status and a valid code; `internalErr` never returns raw error text [errors.go:newProblem] [errors.go:internalErr]
- `writeGitLimitErr` maps `ErrBusy`/`ErrTimeBudget` to 503 and memory, output, or blob limits to 422 [git_budget.go:writeGitLimitErr]
Expand Down
Loading
Loading