Skip to content

feat: align error responses with RFC 9457#79

Open
xernobyl wants to merge 6 commits into
mainfrom
chore/RFC_7807_to_RFC_9457
Open

feat: align error responses with RFC 9457#79
xernobyl wants to merge 6 commits into
mainfrom
chore/RFC_7807_to_RFC_9457

Conversation

@xernobyl
Copy link
Copy Markdown
Contributor

@xernobyl xernobyl commented May 28, 2026

What does this PR do?

Brings the Hub API's error responses up to RFC 9457 (Problem Details for HTTP APIs, obsoletes 7807), with a centralized error-handling architecture optimized for SDK consumers and MCP/agentic clients.

Closes ENG-1058.

Response shape (every error)

{
  "type": "https://hub.formbricks.com/problems/validation",
  "title": "Validation Error",
  "status": 400,
  "detail": "One or more request parameters are invalid",
  "instance": "/v1/feedback-records",
  "code": "validation",
  "request_id": "0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "invalid_params": [
    { "name": "field_type", "reason": "must be one of: text, categorical, nps, csat, ces, rating, number, boolean, date" }
  ]
}

Content-Type: application/problem+json, Cache-Control: no-store, and X-Request-ID: … are set on all error responses.

Highlights

  • RFC 9457 core + extensions. Stable code as a closed enum in the OpenAPI schema. request_id for failure correlation. details for optional structured context, e.g. resource_type on 404. invalid_params:[{name, reason}] with self-correcting reasons that enumerate allowed values and constraints.
  • One shape for field errors. Body validation, path params, query params, search sentinels (tenant_id, query, cursor), query-decode failures, and JSON-decode failures all return validation + invalid_params where appropriate.
  • Centralized error handling. Handlers collapse to response.RespondError(w, r, err); a central mapper translates huberrors, validation, query-decode, JSON-decode, and cursor errors into the right problem response.
  • Cleaner separation of concerns. Validation wording and field-path formatting live in the validation layer; the response layer maps and serializes problem details.
  • Consistent problem+json for routing-level errors. New ProblemErrors middleware rewrites stdlib ServeMux plain-text 404/405 responses into problem+json, preserving Allow on 405.
  • HTTP standards compliance. WWW-Authenticate: Bearer on every 401 (RFC 9110 §11.6.1). Cache-Control: no-store on every problem.
  • Centralized cursor guidance. Cursor recovery wording is shared via response.InvalidCursorReason.

Drive-by

  • Bumped golang.org/x/net v0.53.0 → v0.55.0 (+ aligned crypto/sys/text) to close GO-2026-5026 flagged by the security scan.

Deferred to a follow-up

Aligning naming exactly with the v3 API (request_idrequestId, unauthorizednot_authenticated, optionally collapsing the validation type into bad_request) is intentionally a separate PR. This lands an idiomatic RFC 9457 implementation in Hub-native snake_case first.

How should this be tested?

  • go test ./cmd/api ./internal/api/...
  • go test ./internal/... ./pkg/...
  • golangci-lint run ./... — 0 issues
  • make migrate-validate
  • DB-backed smoke:
    • created temporary hub_smoke database
    • ran goose migrations
    • ran DATABASE_URL='postgres://postgres:postgres@localhost:5432/hub_smoke?sslmode=disable' API_KEY='test-api-key-12345' go test ./tests/... -v -timeout 120s
    • dropped hub_smoke afterward

Manual smoke expectations:

  • POST /v1/feedback-records with bad field_type → 400 validation + invalid_params.
  • GET /v1/feedback-records?tenant_id=org-123&since=not-a-date → 400 validation + invalid_params:[{name:"since", reason:"must be in RFC3339 (ISO 8601) format"}].
  • GET /v1/feedback-records/not-a-uuid → 400 validation + invalid_params:[{name:"id", reason:"must be a valid UUID"}].
  • PUT /v1/feedback-records/<uuid> → 405 problem+json with Allow header.
  • GET /v1/nonexistent → 404 problem+json, not plain text.
  • Missing/invalid Authorization → 401 problem+json with WWW-Authenticate: Bearer.
  • Every error includes Content-Type: application/problem+json, Cache-Control: no-store, X-Request-ID header, and matching request_id body field.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Repository Guidelines
  • Self-reviewed my own code
  • Commented on hard-to-understand bits
  • Ran build/tests/lint relevant to this change
  • Removed debug prints / temporary logging
  • Merged the latest changes from main onto my branch with git pull origin main
  • If database schema changed: added migration and ran make migrate-validate (n/a — no schema changes)

Appreciated

  • If API changed: added or updated OpenAPI spec
  • If API behavior changed: added request/response examples
  • Updated docs in docs/ if changes were necessary (n/a — no doc changes)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 28, 2026

✱ Stainless preview builds

This PR will update the hub SDKs with the following commit message.

chore: Move errors from RFC 7807 to RFC 9457 standards

Edit this comment to update it. It will appear in the SDK's changelogs.

hub-openapi studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅

hub-typescript studio · code · diff

Your SDK build had at least one "note" diagnostic, but this did not represent a regression.
generate ✅build ⏭️lint ⏭️test ✅


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-05-29 10:47:16 UTC

@xernobyl xernobyl changed the title chore: Move errors from RFC 7807 to RFC 9457 standards feat: align error responses with RFC 9457 May 28, 2026
@xernobyl xernobyl marked this pull request as ready for review May 28, 2026 15:28
@xernobyl xernobyl marked this pull request as draft May 28, 2026 15:31
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Review Change Stack

Walkthrough

This PR implements RFC 9457–compliant problem details as the unified error response format across the HTTP API. The changes introduce stable problem type URIs and machine-readable code values, centralize error-to-problem translation to map validation/domain/size-limit errors into structured invalid_params lists, refactor response helpers to accept request context for instance/request-ID propagation and structured logging, add middleware for request body limiting (1 MiB cap) and 404/405 normalization into problem+json, and migrate all HTTP handlers and auth middleware to the new layer. Legacy RFC 7807 response code is removed from validation and tests are updated to verify the new response shape including InvalidParams fields, problem codes, and cache-control headers.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: align error responses with RFC 9457' clearly and concisely describes the main change: updating the API's error response format to comply with RFC 9457 standards.
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 The PR description comprehensively covers what the PR does, why it matters (RFC 9457 alignment), testing approach, and includes detailed examples of the new response shape.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/api/handlers/feedback_records_handler.go (1)

180-192: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Require tenant scope on DeleteByUser.

Line 190 can execute a bulk delete with filters.TenantID == nil, which makes a normal feedback-records endpoint capable of deleting tenant-owned data across tenants. This needs to require tenant_id, or be split into an explicitly named/documented GDPR-style all-tenant erasure flow with separate tests and logging.

As per coding guidelines, "Tenant-owned data must never be read, enqueued, dispatched, cached, searched, embedded, or deleted across tenants without explicit tenant scope verification" and "Do not model tenant_id as an optional filter for tenant-owned resources."

🤖 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 `@internal/api/handlers/feedback_records_handler.go` around lines 180 - 192,
The DeleteByUser handler currently allows bulk deletes when filters.TenantID is
nil; update DeleteByUser to enforce explicit tenant scope by validating
filters.TenantID is non-nil before calling h.service.DeleteFeedbackRecordsByUser
and return a bad-request/authorization error if missing. Locate the filters
variable (models.DeleteFeedbackRecordsByUserFilters) and add a guard after
validation.ValidateAndDecodeQueryParams that checks filters.TenantID (or
equivalent field) and rejects the request if nil, or alternatively split into a
separate explicit "all-tenant erasure" flow with distinct endpoint/logic and
tests; ensure h.service.DeleteFeedbackRecordsByUser is only called when tenant
scope is present.
🤖 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.

Inline comments:
In `@internal/api/handlers/search_handler.go`:
- Around line 67-69: The handler returns 405 without an Allow header; before
calling response.RespondProblem when rejecting non-POST requests (the r.Method
!= http.MethodPost check and the second 405 branch around the later check), set
the response header Allow to "POST" (e.g., w.Header().Set("Allow",
http.MethodPost)) so clients receive the permitted method; update both
occurrences in search_handler.go (the initial method check and the later 405
branch) to set the header prior to responding.

In `@internal/api/middleware/problem.go`:
- Around line 18-32: The problemErrorWriter currently embeds http.ResponseWriter
and overrides WriteHeader/Write but doesn't forward optional interfaces; add
method delegations on problemErrorWriter for http.Flusher (Flush), http.Hijacker
(Hijack), http.Pusher (Push), and io.ReaderFrom (ReadFrom) that check and call
the same method on the underlying ResponseWriter when it implements those
interfaces, returning appropriate results/errors otherwise; update the type to
use the embedded ResponseWriter field (already named ResponseWriter) for
assertions inside each delegated method so existing WriteHeader/Write behavior
is preserved while enabling interface upgrades/streaming for handlers that
require them.

In `@internal/api/response/errors.go`:
- Around line 99-102: The JSON error handling in the function around the
json.SyntaxError check currently misses truncated payloads (io.ErrUnexpectedEOF)
and treats them as 500s; add a check using errors.Is(err, io.ErrUnexpectedEOF)
(or a var ioErr = io.ErrUnexpectedEOF and errors.Is) and return
newProblem(http.StatusBadRequest, "Invalid JSON: "+err.Error()), true when that
matches; ensure the io package is imported if not present and place this check
alongside or just after the existing var syntaxErr check so truncated JSON is
classified as a client (400) error instead of a server error.
- Around line 113-121: Replace the broad substring check for "unknown field"
with a stricter JSON-decoder-origin check: in the branch that currently does if
strings.Contains(err.Error(), "unknown field") (around newValidationProblem /
InvalidParam usage), only treat it as a 400 validation error when the error
clearly comes from the json decoder (e.g., strings.HasPrefix(err.Error(), "json:
unknown field") or, better, use errors.As to match the JSON decode error type if
available). Update unknownJSONField(err) callsite to be invoked only after this
tighter check so unrelated internal errors are not misclassified as validation
problems.

In `@internal/api/response/response.go`:
- Around line 63-69: The response writer sets the problem body RequestID from
context but doesn't mirror it into the response headers; update the writeProblem
flow (where problem.RequestID is assigned and logProblem is called) to also set
the "X-Request-ID" response header to the same value (problem.RequestID or
observability.RequestIDFromContext(ctx)) before writing the body so header/body
parity is guaranteed regardless of middleware ordering.

In `@internal/api/validation/validation_test.go`:
- Around line 58-61: The test only asserts validationErrors[0].Field(), losing
detail-level checks; update the assertions on the validationErrors returned from
ValidateStruct to also assert validationErrors[0].Tag() equals "no_null_bytes"
and validationErrors[0].Value() equals the offending value (the string
containing the null byte) so the test
TestValidateStructPreservesValidationDetails ensures both the rule/tag and the
offending value are preserved; use the existing validationErrors variable and
its methods Tag() and Value() to add these assertions.

In `@openapi.yaml`:
- Around line 1446-1449: The OpenAPI schema defines the components field
invalid_params as an unbounded array; update the items schema for invalid_params
(the array using $ref '`#/components/schemas/InvalidParam`') to include an
explicit maximum size (e.g., add maxItems: 100) to cap the array length and
prevent unconstrained payloads so CKV_OPENAPI_21 is satisfied.

---

Outside diff comments:
In `@internal/api/handlers/feedback_records_handler.go`:
- Around line 180-192: The DeleteByUser handler currently allows bulk deletes
when filters.TenantID is nil; update DeleteByUser to enforce explicit tenant
scope by validating filters.TenantID is non-nil before calling
h.service.DeleteFeedbackRecordsByUser and return a bad-request/authorization
error if missing. Locate the filters variable
(models.DeleteFeedbackRecordsByUserFilters) and add a guard after
validation.ValidateAndDecodeQueryParams that checks filters.TenantID (or
equivalent field) and rejects the request if nil, or alternatively split into a
separate explicit "all-tenant erasure" flow with distinct endpoint/logic and
tests; ensure h.service.DeleteFeedbackRecordsByUser is only called when tenant
scope is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 647e50d5-c883-4be9-84a3-9341c4e77e70

📥 Commits

Reviewing files that changed from the base of the PR and between b0ab202 and 8e12020.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (23)
  • cmd/api/app.go
  • go.mod
  • internal/api/handlers/feedback_records_handler.go
  • internal/api/handlers/feedback_records_handler_test.go
  • internal/api/handlers/openapi_handler.go
  • internal/api/handlers/search_handler.go
  • internal/api/handlers/tenant_data_handler.go
  • internal/api/handlers/tenant_data_handler_test.go
  • internal/api/handlers/webhooks_handler.go
  • internal/api/middleware/auth.go
  • internal/api/middleware/body.go
  • internal/api/middleware/body_test.go
  • internal/api/middleware/problem.go
  • internal/api/middleware/problem_test.go
  • internal/api/response/errors.go
  • internal/api/response/problem.go
  • internal/api/response/response.go
  • internal/api/response/response_test.go
  • internal/api/validation/validation.go
  • internal/api/validation/validation_test.go
  • internal/observability/logging.go
  • openapi.yaml
  • tests/integration_test.go

Comment thread internal/api/handlers/search_handler.go
Comment thread internal/api/middleware/problem.go
Comment thread internal/api/response/errors.go
Comment thread internal/api/response/errors.go Outdated
Comment thread internal/api/response/response.go
Comment thread internal/api/validation/validation_test.go
Comment thread openapi.yaml
@xernobyl xernobyl marked this pull request as ready for review May 29, 2026 10:48
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