Skip to content

Add configurable CORS middleware with defserver integration#30

Open
carpentry-agent[bot] wants to merge 1 commit into
mainfrom
claude/cors-middleware
Open

Add configurable CORS middleware with defserver integration#30
carpentry-agent[bot] wants to merge 1 commit into
mainfrom
claude/cors-middleware

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

The existing CORS module only allowed configuring the origin via CORS.configure. This PR makes the CORS middleware fully configurable and adds a convenient (cors ...) form for defserver.

New API

CORS.setup — configure origin, methods, headers, and max-age in one call:

(CORS.setup @"https://example.com"
            @"GET, POST, OPTIONS"
            @"Content-Type, Authorization"
            @"3600")

CORS.set-credentials! — enable Access-Control-Allow-Credentials:

(CORS.set-credentials! true)

CORS.set-expose-headers! — set Access-Control-Expose-Headers:

(CORS.set-expose-headers! @"X-Request-Id, X-Total")

(cors ...) in defserver — registers both CORS hooks automatically:

(defserver "0.0.0.0" 3000
  (cors @"https://example.com" @"GET, POST" @"Content-Type" @"3600")
  (GET "/" hello))

Or just (cors @"*") for defaults.

Behaviour changes

  • after-hook now adds Vary: Origin when the configured origin is not * (correct cache behaviour per the CORS spec)
  • after-hook conditionally adds Access-Control-Allow-Credentials: true and Access-Control-Expose-Headers
  • before-hook includes Access-Control-Allow-Credentials on preflight responses when credentials are enabled
  • CORS.configure is preserved for backward compatibility

Tests

18 new tests in test/cors_test.carp covering:

  • before-hook pass-through and preflight responses
  • Configurable methods, headers, max-age
  • Credentials on/off for both preflight and normal responses
  • Expose-headers on/off
  • Vary header presence/absence
  • Integration with web-build-response pipeline
  • Backward compatibility of CORS.configure

Note: The existing test/web.carp has a pre-existing C compilation error from the datetime dependency (tm_zone const qualifier mismatch) that is unrelated to this PR. The CORS tests use add-cflag to work around it.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

CORS.setup configures origin/methods/headers/max-age in one call.
CORS.set-credentials! and CORS.set-expose-headers! control optional
headers. The after-hook adds Vary: Origin for non-wildcard origins
and conditionally adds credentials and expose-headers.

A new (cors ...) form in defserver registers both hooks automatically,
replacing the manual (before CORS.before-hook) / (after CORS.after-hook)
pattern. CORS.configure is preserved for backward compatibility.

@carpentry-reviewer carpentry-reviewer 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.

Build & Tests

CI passes on macos-latest. Cannot build locally (no Carp compiler on this machine), relying on CI results. 18 new tests in test/cors_test.carp all pass.

Findings

  1. credentials: true with origin: "*" is not validated. The CORS spec (§3.2.5) says Access-Control-Allow-Origin must not be * when credentials are enabled — browsers will silently reject such responses. The set-credentials! doc mentions this, but the code doesn't warn at configuration time. Consider printing a warning in set-credentials! or setup when both conditions hold. Not blocking, but users hitting this would get confusing browser errors.

  2. Vary header accumulation (minor). after-hook adds Vary: Origin via Response.with-header, which appends to the header's value array. If the application already sets Vary (e.g., Vary: Accept-Encoding), this creates a second array entry (separate header line) rather than comma-joining. Per HTTP spec this is valid, but less clean. Minor, not blocking.

  3. (cors ...) form arity validation (minor). The defserver macro handles arity 2 ((cors @"*")) and arity 5 ((cors @"origin" @"methods" @"headers" @"max-age")), but other arities (3, 4, 6+) would crash at macro expansion with unhelpful car/cdr errors. A compile-time error, not runtime, so not critical — but a guard with a clear error message would improve the experience.

  4. Test workaround is documented. The add-cflag "-Wno-incompatible-pointer-types-discards-qualifiers" in the test file works around a pre-existing datetime dependency issue and is noted in the PR description. Doesn't affect the library itself.

  5. Hook ordering is correct. CORS before-hooks are prepended (preflight short-circuits before user hooks), CORS after-hooks are appended (CORS headers added after user after-hooks).

  6. API design is consistent. The global-state approach (module-level def variables) matches the existing CORS module and how the web framework operates. The defserver (cors ...) integration is clean and well-documented.

  7. CHANGELOG is updated. Good.

  8. Test coverage is solid. Tests cover: before-hook pass-through, preflight 204, configurable methods/headers/max-age, credentials on/off for both preflight and normal responses, expose-headers on/off, Vary header presence/absence, web-build-response pipeline integration, and backward compatibility of CORS.configure.

Verdict: merge

Good feature addition with thorough test coverage. The credentials+wildcard validation gap (finding #1) is the most substantive concern — worth addressing in a follow-up — but it's a documentation-vs-enforcement issue common across CORS libraries, and the docs do warn about it.

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.

0 participants