Request lifecycle
A single Translately HTTP request flows through a fixed chain of JAX-RS filters before reaching the resource method, and a fixed chain of exception mappers on the way back. This page documents the order, the invariants each filter maintains, and the error envelope that leaves the server.
Introduced by: T108 (@RequiresScope + ScopeAuthorizationFilter), T111 (TenantRequestFilter), T103 (auth endpoints), T104 (JWT issuer).
Related: auth architecture, authorization, multi-tenancy, API conventions.
Filter chain (request side)
sequenceDiagram
autonumber
participant C as Client
participant T as TenantRequestFilter<br/>@Priority(AUTHENTICATION - 100)
participant A as JwtSecurityScopesFilter<br/>@Priority(AUTHENTICATION)
participant Z as ScopeAuthorizationFilter<br/>@Priority(AUTHORIZATION)
participant R as Resource method
participant S as Service layer
C->>T: HTTP request
T->>T: extractTenant(uriInfo.path)
T->>T: TenantContext.set(...)
T->>A: continue
A->>A: parse Authorization header
A->>A: verify JWT / API key / PAT
A->>A: SecurityScopes.set(resolved scopes)
A->>Z: continue
Z->>Z: read @RequiresScope on resource
alt scopes cover required
Z->>R: invoke
R->>S: service call (within @Transactional)
S-->>R: result
R-->>C: 200 + JSON body
else scopes insufficient
Z-->>C: 403 INSUFFICIENT_SCOPE
end
Why this order
- Tenant first. Authenticators need to know the tenant to resolve project-scoped credentials (API keys, PATs).
- Auth second. The scope authorization filter relies on
SecurityScopespopulated by a successful authentication; without a credential, it cannot answer “does this caller have the required scope?”. - Authorization third. Deliberately last so all upstream context (tenant, principal, scopes) is available when the
@RequiresScopecheck runs.
Priorities are JAX-RS numeric — lower runs earlier. Priorities.AUTHENTICATION = 1000 and Priorities.AUTHORIZATION = 2000 come from jakarta.ws.rs.Priorities; the tenant filter’s -100 offset guarantees it runs before any authenticator regardless of future additions.
Resource → service → data
- Resource methods are thin: parse path / query / body, call the service, map the return value to a DTO. No transactions at this layer.
- Services are the transactional boundary.
@Transactionalannotates the public method; nested service calls run in the same transaction. - Data access flows through Panache repositories. A single service method is free to issue multiple queries; Hibernate’s first-level cache handles same-session identity.
Exception mapping
Uncaught exceptions land in a JAX-RS ExceptionMapper. The standard mappings:
| Throwable | HTTP | Error envelope code |
Mapper |
|---|---|---|---|
AuthException.InvalidCredentials |
401 | INVALID_CREDENTIALS |
AuthExceptionMapper |
AuthException.EmailNotVerified |
403 | EMAIL_NOT_VERIFIED |
″ |
AuthException.RefreshTokenReused |
401 | REFRESH_TOKEN_REUSED |
″ |
AuthException.ValidationFailed |
400 | VALIDATION_FAILED |
″ |
InsufficientScopeException |
403 | INSUFFICIENT_SCOPE |
InsufficientScopeExceptionMapper |
NotFoundException |
404 | NOT_FOUND |
NotFoundMapper |
ConstraintViolationException (Jakarta Validation) |
400 | VALIDATION_FAILED |
ConstraintViolationMapper |
WebApplicationException |
pass-through | varies | default |
| anything else | 500 | INTERNAL_ERROR |
FallbackExceptionMapper (logs stack, hides detail) |
Every response uses the uniform envelope:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable summary",
"details": { "optional": "extra context" }
}
}
See API errors reference for the full catalogue of code strings.
Observability hooks
- Request ID — Quarkus’s default
x-request-idpropagates through the filter chain and lands in structured logs. If absent, the server generates one. - OpenTelemetry — Quarkus OTel is enabled by default; the tenant identifier is added as a span attribute (
translately.tenant) when bound. - Metrics — Quarkus Micrometer exposes
/q/metrics(Prometheus format). Every resource class getshttp.server.requestscounters with route + status labels.
Cross-cutting tests
TenantRequestFilterITexercises the order: tenant must be bound before authenticators seeContainerRequestContext.ScopeAuthorizationITcovers every combination of present / missing scope and provesINSUFFICIENT_SCOPEwins overNOT_FOUND(we don’t leak whether a resource exists).AuthResourceIT(T103) round-trips the full credential set against Testcontainers Postgres + Mailpit.
When to add a new filter
Rarely. Today’s chain is three links — keeping it small matters because every filter runs on every request. Before adding one, consider:
- Can this live in a service instead?
- Can this be an
ExceptionMappertriggered by a thrown business exception? - If it really must be a filter, what priority relative to the existing three?
A new filter ships with a failing test that proves order (it must run before / after an existing one) and a line in this document.