Skip to content

api: problem-details later tiers (specific codes, instance, result envelope, git/LFS writers, problem+json) — held on SDK trigger #352

Closed

Description

@ThomasK33

Later tiers of RFC 9457 problem-details

Tier 1 (#349, PR #351, merge 1f392ac3) adds type, title, status, a generic per-status code, and detail to the plain {"error"} body. This issue tracks the remaining tiers, held until a trigger fires:

  1. Specific codes such as repository_not_found, branch_not_found, ref_not_found, insufficient_scope and repo_name_mismatch. Some can come from central helpers (resolveRepo, auth middleware); covering them fully means going through each writeErr call site.
  2. The instance field.
  3. Problem fields on the write routes' result envelope ({"result":{"success":false,…}}, ADR-26), where upstream uses problem bodies.
  4. The git smart-HTTP and LFS error writers in internal/githttp.
  5. Switching to application/problem+json. The 1.17.1 Go and TypeScript SDKs parse JSON errors only for application/json. 1.18.0-beta.0 also accepts problem+json, so this becomes possible once the stable SDKs do.

Revisit triggers

These match the RFC 9457 row in docs/explanation/wire-compatibility.md:

  • A stable SDK release that reads code, or that parses application/problem+json (item 5).
  • A user report of depending on a specific code.

The daily upstream compatibility audit checks the SDK release channels, so the desk can re-triage when a trigger fires.

Refs #349, #348.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Activity

  1. ThomasK33 commented on Oct 8, 2026

    @ThomasK33
    MemberAuthor

    New tier: field-level validation (invalid_params), explicitly deferred

    Upstream's 2026-10-08 spec (#396) adds ValidationProblemDetails: ProblemDetails plus an optional invalid_params: [{name, reason}] on about 30 write 400s. name is the dotted wire path of one invalid request-body field. Upstream leaves the member out when it cannot name a field.

    Decision: deferred. open.storage omits invalid_params. error, code and the rest of the envelope do not change.

    Why:

    1. The member is optional, so omitting it is wire-compatible.
    2. No SDK reads it. I searched the Go, TypeScript and Python packages at 1.17.1 (stable) and 1.18.0-beta.1 (beta) and found 0 references.
    3. The 400s come from about 186 call sites and five writers (writeErr, aliasFail, and the result-envelope writers that tier 3 above covers). A partial rollout would cover an arbitrary subset.

    Docs: PR #398 records the omission in docs/reference/http-api.md (Errors) and in the RFC 9457 row of docs/explanation/wire-compatibility.md.

    Revisit triggers for this tier, in addition to the ones above:

    • an SDK release that reads invalid_params, or
    • a user report of depending on it.

    Refs #396.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  2. ThomasK33 commented on Oct 13, 2026

    @ThomasK33
    MemberAuthor

    Plan: later problem-details tiers (code, instance, invalid_params)

    This plan covers four parts of upstream's error contract: specific code values, instance, invalid_params, and the content type. Tier 3 (result-envelope bodies) and tier 4 (git and LFS writers) stay held. The error field and its text do not change: the 1.17.1 SDKs read it.

    The desk owner reopened the invalid_params deferral above (issuecomment-6052209313). This plan replaces that deferral.

    1. One central producer

    internal/api/errors.go gets one problem writer. writeErr becomes a thin wrapper around it with the generic code. Call sites never build a problem body.

    // problem is one RFC 9457 error body. Only writeProblem writes it.
    type problem struct {
    	status        int
    	code          string         // generic code unless a specific code applies
    	msg           string         // goes to `error` and `detail`, unchanged text
    	invalidParams []invalidParam // 400 request-body failures only
    }
    
    type invalidParam struct {
    	Name   string `json:"name"`   // dotted wire path, e.g. base_repo.auth.token
    	Reason string `json:"reason"` // the broken rule, e.g. "is required"
    }

    Validation failures use one error type. A validator or decode helper returns a *fieldError. One writer, writeInvalid(w, err), writes the 400. It reads invalid_params from the error with errors.As. Error() returns the current message, so the error text stays the same.

    type fieldError struct{ name, reason, msg string } // Error() == msg
    func writeInvalid(w http.ResponseWriter, err error) // 400 bad_request (+ invalid_params when err names a field)

    writeProblem asserts these rules:

    • A specific code has the status that upstream lists for it.
    • invalid_params appears only on 400, and each item has a non-empty name and reason.

    instance is the request path, escaped, without the query. Upstream's examples show the same thing: instance: /api/repos for a bad ?cursor. A wrapper on the outermost handler (outside recoverPanics) records the path. writeProblem finds it through the Unwrap() chain that http.ResponseController already uses. The path is captured at the outermost handler because the /api/repos/{repo_name}/... aliases rewrite r.URL.Path to the v1 path before they dispatch (alias.go:212). With this capture, instance is the path that the client sent, and the 228 writeErr call sites do not change.

    2. Validators that can name a field

    Upstream sends invalid_params only for request-body fields. It leaves the member out when it cannot name a field, for example when the body is not valid JSON. This plan does the same.

    Validator Field name source Example {name, reason}
    Strict decode (strictJSON / checkStrictObject) the member path the checker already builds {base_repo.auth, "is not a known field"}, duplicate, must not be null
    DisallowUnknownFields decodes (fork base_repo, create upstream, create-branch) stdlib text json: unknown field "x", prefixed with the member (upstream.x). A test pins this format. {upstream.branch, "is not a known field"}
    Type mismatch on any decode (*json.UnmarshalTypeError.Field) the dotted Field {default_branch, "must be a string"}. This is the example in upstream's spec.
    Required fields literal at the call site default_branch, password, id, ref, base_repo.name, base_repo.auth.token
    Enums and formats literal at the call site default_branch, target_branch, the branch and tag names, password (reserved value)
    Ranges and length limits literal at the call site grep limits.max_lines, limits.max_matches_per_file, pagination.limit

    These failures get no invalid_params: unreadable or empty bodies, JSON syntax errors, rules across two fields (base_repo and upstream cannot both be set, the same as upstream's example), "one of A, B or C is required", query-parameter 400s, and result-envelope 400s (tier 3). The scope is the plain-envelope 400s on the about 15 operations where upstream's spec uses ValidationProblemDetails.

    3. code mapping

    The spec's "Specific codes:" list is the source. A test parses it, the same way as TestGenericProblemCodeMatchesUpstream. Every code that open.storage emits must be in the list with the same status.

    Code Status open.storage source PR
    insufficient_scope 403 requireAuthWith "missing required scope" on plain-body routes 2
    repository_not_found 404 repo lookups that answer "repository not found" (handlers*.go, repourls.go, the alias read flavor) 2
    repo_name_mismatch 400 the 3 repoNameMismatchMsg sites (create, fork, sync create) 2
    branch_not_found 404 "branch not found", and merge's plain "source/target branch X not found" 3
    ref_not_found 404 "ref not found", "tag not found", "commit not found", "base ref not found", "ref does not resolve to a commit" 3
    credential_not_found 404 git-credentials plain bodies 3
    credential_exists 409 git-credentials create 3
    repository_deleted 409 DELETE on a deleted repo 3

    These codes stay generic. Each has a reason:

    • repo_claim_missing: upstream answers 400. open.storage answers 403 ("token is not repo-scoped"). A status change breaks clients, so it is out of scope.
    • authentication_unavailable: open.storage answers 401 on every verifier failure. It has no separate "cannot verify now" path.
    • route_not_found: unmatched /api/ paths reach the mux's plain-text 404 and 405 answers or the git fallback. A catch-all route changes 405 answers. This belongs with tier 4.
    • merge_conflict: merge conflicts use the result envelope (tier 3).
    • sync_in_progress, upstream_already_configured: no handler answers these 409s.
    • repository_thawing, github_*, hosting_*, deployment_*, idempotency_key_reused: open.storage has no such feature.

    "Not found" answers for paths, files, notes, webhooks and organizations keep not_found. Upstream has no specific code for them.

    4. Content type: stays application/json

    The stable SDKs still parse error JSON only for application/json. npm latest is 1.17.1 and beta is 1.18.0-beta.1 (checked 2026-10-13).

    • Go code-storage-go@v1.17.1 fetch.go:91: if strings.Contains(contentType, "application/json"). application/problem+json does not contain that substring, so the SDK falls back to the raw body text as the message.
    • TypeScript @pierre/storage@1.17.1 dist/index.js:1266: if (contentType.includes("application/json")). Otherwise it reads the body as text.
    • Both betas also accept problem+json: Go v1.18.0-beta.1 fetch.go:96, TS 1.18.0-beta.1 dist/index.js:1318.
    • Python pierre_storage 1.17.1 calls response.json() without a content-type check, so it does not decide this.

    Upstream's spec lists application/json next to application/problem+json for every error response, with the same schema. So application/json stays a faithful choice. The switch stays held on the existing trigger: a stable SDK release that accepts problem+json.

    5. Tests on the PG and SQLite lanes

    • Unit tests: the producer's assertions, instance lookup through statusRecorder and gitReadWriter, the panic path, and one test for each decode-failure class in the table in section 2.
    • Spec pins: the specific-code list (both directions for the codes we emit), the InvalidParam fields, and upstream's default_branch / must be a string example. A schema-shape test checks that emitted bodies have no members outside ProblemDetails / ValidationProblemDetails.
    • Full server, both lanes (runBackends: pg and sqlite):
      • requireEnvelope asserts instance equals the client's escaped request path. It already runs on about 85 error assertions, so all of them check instance.
      • One request for each mapped code and each invalid_params class.
      • Negative controls: query 400s and syntax errors carry no invalid_params, and result-envelope bodies do not change.
    • SDK compatibility: sdkcompat_test.go with OPENSTORAGE_SDK_DIR at Go v1.17.1. Dogfood with a real server, curl, and the Go and TS 1.17.1 SDKs reading the error message.

    6. PRs (each at most 500 changed lines)

    1. feat(api): the central writeProblem and instance on every plain problem body. Docs: http-api.md Errors, and the RFC 9457 row in wire-compatibility.md.
    2. feat(api): the specific-code registry pinned to the spec, plus the central-helper codes (insufficient_scope, repository_not_found, repo_name_mismatch).
    3. feat(api): the call-site codes (branch_not_found, ref_not_found, credential_*, repository_deleted).
    4. feat(api): fieldError, writeInvalid and invalid_params for decode failures in the shared decode helpers.
    5. feat(api): invalid_params for required, enum and range validators on upstream's ValidationProblemDetails routes. If this PR goes over 500 lines, it splits by route family (repo create/patch/fork/sync, then refs/credentials/grep/merge).

    problem+json gets no PR. It stays held as described in section 4.

    Acceptance

    • Every plain problem body has instance equal to the request path that the client sent.
    • Every code that open.storage emits is in upstream's table with the same status.
    • invalid_params appears only on 400 bodies with a nameable body field.
    • error and detail text do not change, and the content type stays application/json.
    • The SDK 1.17.1 Go and TS clients still surface the same error message.

    I am starting PR 1 now. This plan needs no product decision: it changes no status codes and no message text.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  3. ThomasK33 commented on Oct 13, 2026

    @ThomasK33
    MemberAuthor

    PR 1 of the plan above landed: #420, merge 6b23ae9f. Plain problem bodies now carry instance.

    • Reviews: one normal+security pair on head c4b79752, both clean. A clean-context advisory recommended "ready".
    • CI: merge-group run 38388515070 (ci) and 38388515047 (pr-title) passed. Push CI on main for 6b23ae9f passed (run 38390906991).

    PRs 2 to 5 (specific codes, then invalid_params) wait for a go-ahead from the desk. This issue stays open.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  4. ThomasK33 commented on Oct 13, 2026

    @ThomasK33
    MemberAuthor

    Progress on the #352 plan: PR 2 and PR 3 landed.

    PR Merge Merge-group runs Main push CI Codex assessments Advisory
    #428 central codes (insufficient_scope, repository_not_found, repo_name_mismatch) b14fa588 ci 38403055977, pr-title 38403055944 38405631607 green 4 (round 1: 2 P2s about fork-source codes, fixed; round 2 clean) ready
    #435 call-site codes (branch_not_found, ref_not_found, credential_not_found, credential_exists, repository_deleted) 5cadc73d ci 38414063759, pr-title 38414064474 38417381756 green 4 (round 1: 1 P2 fixed, 1 P2 rejected because it changes a status code; round 2 clean) ready

    A re-delete through the DELETE /api/repos/{repo_name} alias stays 404 repository_not_found, because its name lookup skips deleted repositories. docs/reference/http-api.md records this.

    Next: PR 4 (invalid_params on decode failures), then PR 5 (single-field validators).


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  5. ThomasK33 commented on Oct 13, 2026

    @ThomasK33
    MemberAuthor

    PR 4 of the plan landed: #436, merge 66f5c329. A 400 for a request body that fails to decode now names the field in invalid_params. Examples: wrong JSON type, including nested base_repo.* and upstream.* members and array elements such as paths.0, and unknown, duplicate, null or non-object members of strict bodies.

    • Merge-group runs: ci 38431551836 and pr-title 38431551616 passed. Main push CI 38433702162 passed.
    • Codex: 4 assessments. Round 1 raised one security P3 and three normal P2s. I fixed the P3 and two P2s. One P2 was a duplicate of the P3. I rejected the element-path claim with probe evidence and pinned element paths in tests. Round 2 was clean in both lanes.
    • Clean-context advisory: ready. The PR is 528 changed lines, over the 500-line plan limit. The extra lines came from the fix that round 1 asked for.

    Next: PR 5 (single-field validators). It also carries the advisory follow-ups.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  6. ThomasK33 commented on Oct 13, 2026

    @ThomasK33
    MemberAuthor

    The #352 plan has shipped. One item stays held.

    All five PRs from the plan (issuecomment-6143529650) have landed. error and detail text, status codes and the application/json content type did not change. Every code change goes from the generic code to upstream's specific code.

    PR What shipped Merge
    #420 instance on every plain problem body: the client's escaped request path, with the alias path for /api/repos/{repo_name}/... 6b23ae9f
    #428 central writeProblem producer, plus insufficient_scope, repository_not_found, repo_name_mismatch b14fa588
    #435 branch_not_found, ref_not_found, credential_not_found, credential_exists, repository_deleted 5cadc73d
    #436 invalid_params on body decode failures: wrong JSON type, including nested members and array elements, and strict unknown, duplicate, null or non-object members 66f5c329
    #443 invalid_params from single-field validators: required fields, allowed values, ranges, name formats on the ValidationProblemDetails routes e1beeeb8

    A test pins every specific code that open.storage emits to the vendored spec's "Specific codes:" list. A test also pins the problem member set to ProblemDetails plus invalid_params. I ran dogfood A/B checks with the stable Go and TS SDKs (1.17.1) on each PR. The SDK output did not change.

    Still held: application/problem+json

    The content type stays application/json. The stable SDKs (1.17.1, npm latest as of 2026-10-13) parse error JSON only for application/json. Only the 1.18.0-beta clients also accept application/problem+json. Revisit trigger: a stable SDK release that accepts application/problem+json. This issue stays open for that item.

    Recorded limits (no follow-up issues)

    • These upstream codes are not emitted, because each would need a status change, a new body shape or a feature open.storage lacks: repo_claim_missing (open.storage answers 403), authentication_unavailable, route_not_found, merge_conflict (result envelope), sync_in_progress, upstream_already_configured, and the hosting, deployment, GitHub App and idempotency codes.
    • A re-delete through DELETE /api/repos/{repo_name} answers 404 repository_not_found, not 409 repository_deleted. The alias's name lookup skips deleted repositories.
    • invalid_params is left out for mutually exclusive members (base_repo with upstream), query parameters, delete-branch's name (query or body), and grep's "cursor past the matches" 400.
    • Result-envelope bodies (tier 3) and the git and LFS writers (tier 4) carry no problem fields.

    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  7. ThomasK33 commented on Oct 13, 2026

    @ThomasK33
    MemberAuthor

    Summary: PRs 1–5 shipped. Items 3, 4 and 5 of this issue stay held.

    This comment corrects my previous one (issuecomment-6152364859), which said that only one item stays held. Against the tier list in this issue's body:

    Shipped

    Still held. This issue stays open for them:

    • Item 1, partly. The remaining specific codes each need a status change, a new body shape or a feature open.storage lacks: repo_claim_missing, authentication_unavailable, route_not_found, merge_conflict (result envelope), sync_in_progress, upstream_already_configured, and the hosting, deployment, GitHub App and idempotency codes.
    • Item 3. Problem fields on the write routes' result envelope ({"result":{"success":false,…}}, ADR-26).
    • Item 4. The git smart-HTTP and LFS error writers in internal/githttp.
    • Item 5. application/problem+json. The stable SDKs (1.17.1) still parse error JSON only for application/json.

    The revisit triggers in the issue body do not change.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  8. ThomasK33 commented on Oct 18, 2026

    @ThomasK33
    MemberAuthor

    Upstream audit 2026-10-17: the docs spec now gives the merge 409 two bodies. An application/json MergeConflict body reports file conflicts and stale-target retries. A ProblemDetails body (code conflict) reports every other conflict. Our merge failures all use the result envelope as application/json (handlers_write_merge.go:128-143; recorded deviation wire-compatibility.md:203). This belongs to items 3 and 5 here and stays gated on G1 (no stable SDK accepts problem+json yet; stable is 1.17.1).


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium

  9. ThomasK33 commented on Oct 27, 2026

    @ThomasK33
    MemberAuthor

    Follow-up from #568 (PRs #571 and #573), item 3 of this issue:

    The four 412 deviation rows (commit-pack, diff-commit, restore-commit, notes body forms) keep their #533 reasons. Their wire-compatibility rows now name ref_guard_failed.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  10. ThomasK33 commented on Oct 29, 2026

    @ThomasK33
    MemberAuthor

    Desk decision: redefine gate G1 (2026-10-29 audit)

    Upstream's unmerged SDK branch necolas/code-storage-pointer says the @pierre/storage / pierre-storage / code-storage-go line gets no new release, and names @pierre/sdk, pierre-sdk and github.com/pierrecomputer/sdk/go as replacements. @pierre/sdk 0.1.0-canary.3 (npm, Oct 27) already parses every error body as JSON, so it accepts application/problem+json.

    G1 now fires on the first stable (non-canary, non-beta) release of @pierre/sdk, pierre-sdk or github.com/pierrecomputer/sdk/go, or on a stable @pierre/storage release that accepts problem+json, whichever comes first. A README change or a canary does not fire it. The daily upstream audit watches the npm dist-tags, PyPI and the Go proxy for these packages.

    Note for the rest of this issue: the canary picks RefGuardError by code === "ref_guard_failed", so our #533 412 deviations will matter once it ships.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium

  11. ThomasK33 commented on Nov 4, 2026

    @ThomasK33
    MemberAuthor

    Gate G1 fired on 2026-11-04. Stable releases: npm @pierre/sdk 0.1.0 (latest), PyPI pierre-sdk 0.1.0 and Go github.com/pierrecomputer/sdk/go v0.1.0 (@latest, tag go/v0.1.0 = 5f1a4bc). The upstream changelog entry pierre-sdk-and-cli (2026-11-03) says the old SDKs get no new releases, and the pointer branch merged as SDK #66.

    SDK 0.1.0 parses every error body as JSON whatever the content type (Go go/errors.go:186-210 at go/v0.1.0), so it accepts application/problem+json, and it branches on ref_guard_failed.

    The desk is assigning owners now. The canonical routes SDK 0.1.0 calls are tracked separately in #656.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium

  12. ThomasK33 commented on Nov 4, 2026

    @ThomasK33
    MemberAuthor

    Item 5: upstream documents application/problem+json for problem bodies. I am switching.

    Answer to the open question (checked 2026-11-04 against the vendored spec 412b90c3 on main 63f81faa):

    1. The spec's Errors section (docs/upstream/openapi.yaml:3027) says: "Most endpoints return RFC 9457-compatible problem details under application/problem+json".
    2. 661 error responses list both application/json and application/problem+json. 646 of them give both the same ProblemDetails schema.
    3. In the other 15 (the write 409s of commit-pack, diff-commit, merge, notes, notes/append, reset-commits and restore-commit, both route forms), the media type separates the two body shapes. application/problem+json carries the problem body (CommitWriteRefGuardProblemDetails, NotesWriteRefGuardProblemDetails, RestoreCommitRefGuardProblemDetails, ProblemDetails). application/json carries the result envelope (CommitWriteResponse, MergeConflict) or plain ProblemDetails.
    4. 25 error responses list only application/json. None is a problem body: CommitWriteResponse (14), ReposCommitsRestoreResponse (8), ReposUpstreamDeleteResponse (3).
    5. Live check: an unauthenticated GET https://api.pierre.code.storage/api/repos answers 401 with Content-Type: application/problem+json for no Accept, Accept: application/json, Accept: application/problem+json and Accept: */*. Upstream does not negotiate.

    What each SDK line does with a application/problem+json body:

    SDK Behavior
    0.1.0 Go (go/runtime.go:386-395, go/errors.go:187-210 at go/v0.1.0) Reads every error body as JSON, whatever the content type. No change.
    0.1.0 Python (pierre/_runtime.py:393-400) Same: json.loads of the body, no content-type check. No change.
    0.1.0 TypeScript (dist/runtime/http.js:467) Same: JSON.parse of the body text. No change.
    1.18.0-beta Accepts both types (plan comment above).
    1.17.1 Go (fetch.go:91, notes util.go:154) and TypeScript (dist/index.js:1266, notes :2131) Parse JSON only when Content-Type contains application/json. On a problem+json body, APIError.Message / ApiError.message becomes the raw body text instead of error, and Body becomes that string. A note write's 409 guard body becomes a generic API error, not the note result.
    1.17.1 Python Calls response.json() with no content-type check. No change.

    All three 0.1.0 SDKs send Accept: application/json on JSON operations. Upstream answers problem+json to that request (point 5), so open.storage will too.

    Decision (desk rule: follow what the spec documents): every body that newProblem builds switches to application/problem+json. That covers writeErr, writeProblem, writeInvalid, the ref_guard_failed bodies (merge, notes append, the notes DELETE query form) and the git-surface 429. These stay application/json: success bodies, result envelopes (commit-pack, diff-commit, restore, reset, merge results and their failures), and the internal/githttp {"error"} writers (item 4). The PR is marked breaking, with a BREAKING CHANGE: line that names the 1.17.1 Go and TS effect.

    Evidence (spec counts script, probe transcript) is in my scratch dir; the PR links back here.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  13. ThomasK33 commented on Nov 4, 2026

    @ThomasK33
    MemberAuthor

    Item 3: scope, from the vendored spec 412b90c3

    I checked every write route of the spec. Upstream documents a plain ProblemDetails body (no result) for 401, 403, 404, 429, 500 and 503 on every write route. It documents a result-envelope body only here:

    Route Statuses with the envelope or another non-problem body
    commit-pack 400, 409, 412, 413, 502 (CommitWriteResponse)
    diff-commit 400, 409, 412, 502 (CommitWriteResponse)
    restore-commit 412, 502 (ReposCommitsRestoreResponse). 400 and 409 are ProblemDetails.
    merge 409 file conflicts and stale-target retries (MergeConflict: error, conflict_type, conflict_paths, merge_base_sha, no result). Every other status is ProblemDetails.
    notes, notes/append, DELETE notes none. Every failure is ProblemDetails, except the 409 guard body (NotesWriteRefGuardProblemDetails), which open.storage already sends.
    branches, tags, git-credentials (path-id) none

    open.storage sends the ADR-26 result envelope on all of these: auth 401/403, the write-route 429, the alias and git-credentials failures, and every wcFail, wmFail and wrefNoteFail status.

    Why this matters now: SDK 0.1.0 Go (go/errors.go:209-275 at go/v0.1.0) turns any error body that has a result member into a RefUpdateError, before it looks at the status. So a 401 on merge gives *pierre.RefUpdateError with the message "401: request failed" and no code, where upstream gives AuthenticationError with code and detail. My item 5 dogfood shows this on branches/create, merge and notes/append.

    Decision (#234 rule: implement only what upstream documents): where upstream documents ProblemDetails, open.storage sends the plain problem body and no result. Where upstream documents the envelope, the envelope stays. Each PR is marked breaking for SDK 1.17.1, which builds a RefUpdateError from result.status on some of these statuses. Three PRs:

    1. Shared writers: write-route auth 401/403, the write-route 429, and the alias and git-credentials failures.
    2. Commit writes (commit-pack, diff-commit, restore-commit): the statuses outside the table above.
    3. Merge and notes: plain problem bodies outside the table, and the MergeConflict shape for merge conflicts.

    Not in item 3: the stale-guard 412s of the #533 deviations. #647 part 2 moves them to 409 ref_guard_failed, and another owner works on it.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  14. ThomasK33 commented on Nov 4, 2026

    @ThomasK33
    MemberAuthor

    Item 4 (git smart-HTTP and LFS error writers): no change needed

    Upstream documents no problem bodies for the git surface, so open.storage changes nothing in internal/githttp for this issue (#234 rule: implement only what upstream documents). Evidence, checked 2026-11-04:

    1. The vendored spec (docs/upstream/openapi.yaml, ee8092bf on main) has only /api/... paths. It documents no smart-HTTP or LFS route and no error body for them.
    2. The vendored guides (docs/upstream/guides/git-operations.md, git-lfs.md) and the live pages docs/repos/git-operations.md and docs/repos/git-lfs.md describe no error body. The live API overview, which defines the RFC 9457 error contract, covers the HTTP API and does not mention the git surface.
    3. Upstream's live git surface sends no problem body:
      • GET https://pierre.code.storage/nope.git/info/refs?service=git-upload-pack without credentials: 401, Content-Type: text/plain; charset=utf-8, body Authentication required.
      • GET https://pierre.code.storage/nope.git/info/lfs/objects/batch without credentials: 401, Content-Type: application/vnd.git-lfs+json, body {"message":"basic auth credentials are required"}.
    4. open.storage's LFS writer (lfsError, internal/githttp/lfs.go) already sends the git-lfs {"message"} body with application/vnd.git-lfs+json. That is the shape the git-lfs client reads and the shape upstream sends.

    Two related facts, outside this item:

    • The git-surface 429 comes from the shared REST limiter (api.NewGitRateLimit, writeErr), so it already carries the problem members. Since feat(api)!: send application/problem+json on problem bodies #660 it is application/problem+json. Upstream documents no git-surface 429 body.
    • open.storage's smart-HTTP errors use {"error"} JSON (httpError, internal/githttp/server.go), where upstream sends text/plain. Git clients print the body as text either way. That is not a problem-details question, so I did not change it here.

    Item 4 is closed by this evidence.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  15. ThomasK33 commented on Nov 4, 2026

    @ThomasK33
    MemberAuthor

    Done: items 5, 3, 1 and 4

    All remaining items of this issue have landed. #672 closed the issue.

    Item PR Merge commit What changed
    5 #660 3c6bfde3 Problem bodies are application/problem+json.
    3, part 1 #663 e0a48fbe Auth (401, 403), the gateway 429 and the path-style alias failures use the plain problem body on every route, write routes included.
    3, part 2 #669 74c96847 commit-pack, diff-commit and restore-commit send the result envelope only on the statuses upstream documents for it, and the plain problem body otherwise, with repository_not_found, branch_not_found and ref_not_found on the 404s.
    3, part 3 #670 3c4b2b57 Merge file conflicts answer upstream's MergeConflict. Every other merge and note-write failure is the plain problem body. A non-fast-forward ff_only merge and unrelated histories answer 412 precondition_failed. A target that moved during a merge answers 409 conflict, because open.storage has no automatic retry. Single-field 400s name the field in invalid_params.
    1 #672 8638fa95 404 route_not_found and 405 method_not_allowed (with Allow) problem bodies for unmatched /api/ paths, and 403 authentication_unavailable when the key store fails (REST and fork source tokens).
    4 none none No change: upstream documents no problem bodies for the git surface (comment).

    Codes that stay unsent, with the reason:

    1. repo_claim_missing: since fix(auth): accept any non-empty sub and treat a token without repo as org-wide #666, a token without a repo claim is org-wide, and a per-repo route refuses it with 403 forbidden.
    2. merge_conflict: the merge file-conflict 409 is MergeConflict, which has no code member.
    3. target_moved and repo_changed: upstream sends them only after an automatic retry, which open.storage does not do.
    4. sync_in_progress, upstream_already_configured: no upstream operation documents them.
    5. Hosting, deployment, GitHub App, cold-storage and idempotency codes: open.storage lacks those features.

    Related: #664 (upstream answers an invalid or expired token with 403, not 401) is open for desk triage. #673 is a comment-only follow-up from the #672 review.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions