Skip to content

fix(api): reject unsupported exchange methods before side effects - #101

Closed
seonghobae wants to merge 6 commits into
mainfrom
fix/exchange-method-short-circuit-on-main
Closed

fix(api): reject unsupported exchange methods before side effects#101
seonghobae wants to merge 6 commits into
mainfrom
fix/exchange-method-short-circuit-on-main

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Purpose

Make /exchange method validation fail closed before credential-egress configuration or distributed rate-limit state is consulted, while preserving HTTP method semantics. Unsupported methods return the public 405 contract without leaking configuration/limiter availability or consuming limiter capacity; HEAD returns the same status/representation metadata without response content.

Exact source identity

  • protected base: main at c85d710804139c0697d7ef8fa47d02b1389e6d84;
  • head branch: fix/exchange-method-short-circuit-on-main;
  • original RED: 4b4af1506aad1e0d67e408afa198eeafbb7ba006;
  • first GREEN: 0572ace139819fb41f9507ca72ca430691a5a045;
  • protocol/security header hardening: 50af2026b30693524d5307b4e9449c888d4dc8b8;
  • complete unsupported-method matrix: 657652cd573f9dd00c148e7bd6ed4b351e2c48a3;
  • bodyless-HEAD RED: 4df1b1983a00fda499b5cae5f402b9af3bbd2c18;
  • current bodyless-HEAD GREEN: 6e49407fddef1ad493a66341537560d1dfe19a6e.

RCA -> RED -> GREEN

The original repair moved method validation to the outer /exchange entrypoint before OIDC envelope/body checks, GitHub egress configuration, outbound-policy activation, and the distributed limiter. A throwing limiter sentinel and intentionally invalid GitHub API base prove unsupported methods short-circuit those side effects.

CodeRabbit then correctly noted that the new 405 security/protocol headers were not pinned. The regression was hardened to assert content type, no-store, Pragma, nosniff, trace presence, and body/header trace-ID consistency.

A later method-matrix audit added HEAD, but the shared JSON helper still constructed a response body for HEAD. The first failing boundary was exchangeMethodResponse: the route correctly rejected HEAD with 405 before side effects, but the returned Response still contained JSON content.

Exact RED 4df1b1983a00fda499b5cae5f402b9af3bbd2c18 adds a dedicated HEAD /exchange regression that preserves the same 405/Allow/security/trace metadata but requires an empty response body. Application ci run 31423347385, job 93569113127, checked out that exact head and failed exactly the new assertion; 652 other tests passed.

Current GREEN 6e49407fddef1ad493a66341537560d1dfe19a6e keeps the same structured 405 payload for non-HEAD methods and passes null response content only for HEAD. This is deliberately narrow; POST behavior and every credential-bearing path are unchanged.

Current exact-head evidence

For unchanged head 6e49407fddef1ad493a66341537560d1dfe19a6e against protected base c85d710804139c0697d7ef8fa47d02b1389e6d84:

  • application ci run 31423489615: terminal success, exact checkout verified;
  • application suite: 63 files / 653 tests passed;
  • configured owned production statements/branches/functions/lines: 100% / 100% / 100% / 100%;
  • npm audit --audit-level=high: 0 vulnerabilities;
  • reviewer-ci run 31423489534: terminal success;
  • central Security Scan run 31423489537: terminal success under current protected-base scanner semantics;
  • current inline review threads: 0 at the latest fresh read;
  • visible formal CodeRabbit review remains COMMENTED evidence anchored to predecessor 0572ace139819fb41f9507ca72ca430691a5a045, not qualifying approval;
  • GitHub reports the PR mergeable and branch 6 commits ahead / 0 behind protected main.

The ordinary release verification still reports non-strict KPI verification as SKIP because no real retained production KPI log is present, and the acquisition manifest continues to identify missing final-gate production/release/deployment/governance/revenue/transfer evidence. Those states are not promoted to operational or acquisition success.

Behavioral boundary

GET, HEAD, PUT, PATCH, DELETE, and OPTIONS /exchange fail before they can expose GitHub API trust/egress state, consume distributed rate-limit capacity, invoke a Durable Object limiter, or enter credential-bearing GitHub/OIDC work. HEAD carries no response content; the other unsupported methods retain the structured JSON error body. POST behavior, body/JWT bounds, egress policy, rate limiting, replay protection, credential minting, dependencies, workflow permissions, licensing, release, and deployment authority are unchanged.

Merge / operational boundary

Technical exact-head success is not merge authority. Live enforceable governance under #27 and an eligible qualifying independent non-author formal-review path under #29 remain unverified/fail-closed. No self-approval, protection weakening, synthetic status, repair workflow, release, deployment, outbound-license decision, or acquisition-readiness claim is made.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

/exchange는 POST가 아닌 요청에 표준 JSON 405 응답을 반환합니다. 메서드 검사는 OIDC 검증과 본문 처리보다 먼저 실행됩니다. 테스트는 허용 메서드, 응답 필드, rate limiter 미호출을 검증합니다.

Changes

/exchange 메서드 계약

Layer / File(s) Summary
비POST 요청 405 처리
src/entrypoint.ts
비POST /exchange 요청에 ERR_VALIDATION_INPUT, Allow: POST, 추적 ID, 보안 헤더 및 캐시 제어 헤더를 포함한 405 JSON 응답을 반환합니다.
메서드 계약 검증
test/exchange-method-contract.test.ts
GET, PUT, PATCH, DELETE, OPTIONS 요청의 응답 구조와 405 상태를 검증합니다. 외부 자격 증명 설정과 분산 rate limiter가 호출되지 않는지도 검증합니다.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 부작용 전에 지원되지 않는 /exchange 메서드를 거부하는 주요 변경 사항을 정확하고 간결하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/exchange-method-short-circuit-on-main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae marked this pull request as ready for review August 10, 2026 14:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/exchange-method-contract.test.ts (1)

23-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

새 405 응답 헤더도 회귀 테스트로 고정하세요.

Line 23-35의 expectMethodNotAllowed는 상태 코드, Allow, JSON 본문만 확인합니다. 구현은 content-type, cache-control: no-store, pragma: no-cache, x-content-type-options: nosniff, x-trace-id를 추가했습니다. 이 검증이 없으면 보안, 캐시 제어, 추적 헤더가 제거되어도 테스트가 통과합니다.

응답 본문을 변수에 저장한 뒤 필수 헤더와 body.trace_idx-trace-id의 일치를 확인하세요.

As per coding guidelines: test/**/*.ts: “Add or update regression tests for security and API behavior changes; use Vitest and preserve assertions covering token non-disclosure and protocol contracts.”

제안하는 테스트 변경
 async function expectMethodNotAllowed(response: Response): Promise<void> {
   expect(response.status).toBe(405);
   expect(response.headers.get("allow")).toBe("POST");
-  expect(await response.json()).toMatchObject({
+  expect(response.headers.get("content-type")).toBe("application/json; charset=utf-8");
+  expect(response.headers.get("cache-control")).toContain("no-store");
+  expect(response.headers.get("pragma")).toBe("no-cache");
+  expect(response.headers.get("x-content-type-options")).toBe("nosniff");
+  const traceId = response.headers.get("x-trace-id");
+  expect(traceId).toEqual(expect.any(String));
+  const body = await response.json();
+  expect(body).toMatchObject({
     ok: false,
     error_code: "ERR_VALIDATION_INPUT",
     message: "Method not allowed",
     details: {
       allowed_methods: "POST",
       hint: expect.any(String),
     },
   });
+  expect(body.trace_id).toBe(traceId);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/exchange-method-contract.test.ts` around lines 23 - 35, Update
expectMethodNotAllowed to store the parsed response body, then assert the
required content-type, cache-control, pragma, x-content-type-options, and
x-trace-id headers. Preserve the existing 405, Allow, and JSON contract
assertions, and additionally verify that body.trace_id matches the x-trace-id
header.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/exchange-method-contract.test.ts`:
- Around line 23-35: Update expectMethodNotAllowed to store the parsed response
body, then assert the required content-type, cache-control, pragma,
x-content-type-options, and x-trace-id headers. Preserve the existing 405,
Allow, and JSON contract assertions, and additionally verify that body.trace_id
matches the x-trace-id header.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 96ff38d6-db09-4315-a9b7-a39aea9fdb7b

📥 Commits

Reviewing files that changed from the base of the PR and between c85d710 and 0572ace.

📒 Files selected for processing (2)
  • src/entrypoint.ts
  • test/exchange-method-contract.test.ts

Copy link
Copy Markdown
Contributor Author

Superseded by #214 on the current protected-main lineage.

Fresh proof immediately before this decision:

#101's complete two-file delta is preserved byte-for-byte on #214:

  • src/entrypoint.ts: blob cca10aba46b92cc21662fa18a63e5ae9035978c5 on both heads;
  • test/exchange-method-contract.test.ts: blob a58377a74c186bf81499042be834b573487feb5c on both heads.

#101 has no current inline review threads to preserve. #214 also has zero current inline review threads and its exact current head has fresh terminal-success ci 31577641753, reviewer-ci 31577641742, and eligible central Security Scan 31577641765. Those current-head results do not transfer backward to #101.

Closing #101 unmerged removes the historical duplicate without losing behavior, test coverage, or review state. #214 remains the sole current-main implementation owner and is otherwise left untouched in this invocation because its branch moved during the fresh writer-state sweep.

Copy link
Copy Markdown
Contributor Author

Superseded by protected-main integration through PR #221.

Fresh pre-closure proof:

  • protected main: 1fbe857a5cf52b5af31e2db5e4676876289e3e23;
  • this PR exact head: 6e49407fddef1ad493a66341537560d1dfe19a6e;
  • integrated production blob is byte-identical: src/entrypoint.ts cca10aba46b92cc21662fa18a63e5ae9035978c5;
  • protected regression blob is a strict successor of this PR's test: this PR has a58377a74c186bf81499042be834b573487feb5c, while protected main has dbc69d5dbd4ad794ce603070ca53599b861a3251, adding the valid X-Latency-Ms: 0 review contract without weakening any existing assertion;
  • current inline review threads on this PR are empty.

No predecessor checks/reviews transfer. The merged protected line preserves the complete runtime fix and a stronger executable contract, so this stale PR is closed unmerged as superseded.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant