vernac / architecture, expressed.
Browse documentation

Outbound HTTP runtime and integration failures

Status: implemented runtime foundation. REST declarations now generate request/response bindings and support external Java error translation. Ordered declarative error rules are implemented. Java adapter behavior can also use this API. See the executable tutorial.

Dependencies and scope

vernac-runtime owns transport-independent integration exceptions. vernac-runtime-http depends on vernac-runtime-json and uses the JDK HTTP client. HTTP/JSON dependencies do not leak into the domain runtime. Applications add the HTTP module explicitly, at the same version as the compiler. No Spring starter, autoconfiguration, database transaction, retry scheduler or new DSL keyword is introduced by this step.

A generated adapter receives an application-managed HttpAdapterClient through java uses, just like other technical dependencies. Inline and implemented by behavior retain the same self access contract. A client is reusable across concurrent calls; invocation/response/error data is call-local. The application owns and closes the underlying JDK HttpClient.

Structured failures

Type Meaning
PortRejectedException Explicitly recognized business rejection; extends VernacDomainException
PortInvocationException Technical integration failure with responsibility not established
LocalIntegrationException Established local integration problem
RemoteIntegrationException Remote failure or response-contract violation
IntegrationCommunicationException Communication failed; remote outcome/responsibility may be unknown

The last three extend PortInvocationException, which extends VernacTechnicalException. All implement PortFailure, exposing invocation() and an immutable transportDetails() map. AdapterInvocation supplies the invocation UUID, fully qualified port/adapter names, operation and deliberately selected request details. Its withDetail method returns a new context.

Technical exceptions have category() of type IntegrationCategory: CONFIGURATION, INVALID_REQUEST, INVALID_RESPONSE, AUTHENTICATION, AUTHORIZATION, RATE_LIMITED, REMOTE_FAILURE, UNEXPECTED_STATUS, TIMEOUT, COMMUNICATION, INTERRUPTED. Observation, responsibility and safe retry remain separate questions.

Business rejections carry an actual enum constant, not an ad-hoc string code. code() retains its declaring enum type. code(MyRejection.class) returns an Optional of that type and is empty for another enum, even with an identical constant name. Transport-independent failures can be used by filesystem, device, other-database and other adapters without HTTP classes.

No exception automatically captures parameters, request URI/query, credentials, cookies, payloads or all response headers. HTTP failure metadata contains only http.status and, when present, http.retryAfter. This is raw advisory information, not a retry instruction. A named correlation header may explicitly be copied to http.correlationId. Select a non-secret header. Application-provided messages, details and causes remain the application’s responsibility.

HTTP call contracts

HttpAdapterClient takes an application-owned JDK client, a positive default request timeout and a positive maximum buffered response size in bytes. Automatic redirects must be disabled. Endpoint/authentication/TLS/proxy configuration belongs to application configuration. No URLs or credentials are embedded in generated code.

API Successful response Non-success response
required Decode/map a 2xx response to a non-null result Run error handler
optionalOn404 Decode/map a 2xx response, wrap in Optional 404 alone becomes absence; otherwise error handler
execute Any 2xx succeeds; ignore bounded body Run error handler

These methods accept a request supplier and an error handler. Keeping construction, DTO creation, mapping and encoding inside the supplier allows the request boundary to classify known contract failures correctly. GET, POST, PUT, PATCH and DELETE are forwarded as requested; the runtime does not translate PATCH into POST.

An optional port without a 404 policy can use Optional.of(required(...)). Successful empty bodies, JSON null and malformed data are never implicitly absent. A void operation deliberately does not decode its response; a 204 cannot satisfy a required JSON value. This initial runtime accepts all 2xx success statuses; operation-specific success sets can be considered with REST syntax if needed.

The response limit applies while bytes arrive, also without Content-Length. It applies to success and error bodies. Exceeding it aborts reception and raises INVALID_RESPONSE with the received status; normal error-body mapping does not run for this resource-limit failure. HttpResponseData defensively copies body bytes; its toString includes status/size, never payload or headers.

A request’s explicit timeout wins; otherwise the configured default applies. The whole HTTP future, including the response body, is bounded. Relying only on JDK HttpRequest.timeout did not bound a stalled body in the tested environment. Timeout cancels the future. Interruption cancels it and restores the thread’s interrupt flag. Both leave the remote operation’s outcome uncertain. Request preparation and application decoding/mapping are outside this network deadline.

The public API is synchronous even though the implementation uses sendAsync plus a bounded wait to cover the body. No application futures escape the runtime. Vernac makes one transport call and has no retry loop. JDK-level transport recovery is not an exactly-once delivery guarantee. A database rollback cannot undo a remote action; use explicit idempotency/protocol design where required.

JSON and mapping boundaries

HttpJson owns a private, preconfigured ObjectMapper; it does not change the application’s mapper. Generated DTO annotations provide strict types, required fields, unknown-property tolerance, optional and PATCH semantics. No guarantee of those semantics is made for arbitrary handwritten Jackson classes.

JSON responses require application/json or application/*+json, ignoring case and optional media-type parameters. Missing or different media types fail. Empty input, JSON null and trailing JSON values fail. A generated DTO contract failure retains its field path. Raw Jackson parser errors are replaced with a sanitized contract error so source excerpts are not copied into exceptions. A known generated mapping or DTO failure retains its useful cause; arbitrary programmer exceptions are not blanket-converted into integration failures.

Boundary Known contract failure Result
Request supplier DTO, mapping or domain validation Local INVALID_REQUEST, original cause
Successful response decoder/mapper DTO, mapping or domain validation Remote INVALID_RESPONSE, status and original cause
Successful decoder returns null Result-contract failure Remote INVALID_RESPONSE
Error handler Explicit business/technical exception Returned exception is thrown unchanged
Arbitrary implementation defect E.g. IllegalStateException Propagates unchanged

JSON syntax errors are sanitized as described above rather than preserving raw parser excerpts. Exceptions emitted deliberately by behavior are not subsequently converted into a different business/technical category.

Default status handling and error bodies

Status Default
401 Technical AUTHENTICATION, responsibility unassigned
403 Technical AUTHORIZATION, responsibility unassigned
429 Technical RATE_LIMITED, responsibility unassigned
5xx Remote REMOTE_FAILURE
Other non-2xx, including 3xx/400/404 Technical UNEXPECTED_STATUS, responsibility unassigned

Only explicit error translation creates a business rejection or assigns an authentication/authorization failure to local configuration. HttpFailures.defaultFailure provides these defaults. It does not parse error bodies and never retries.

HttpJson.errorBody returns DecodedError<T>. A malformed/missing/unusable body has no value and supplies a sanitized decoding failure. It does not replace the original HTTP status failure. A Java handler can attach the decoding failure as a suppressed exception and still match status-only conditions. Unrelated application programming errors propagate rather than being treated as unavailable wire data.

ErrorCondition defines three-valued evaluation for declarative rules:

A B A AND B A OR B
true true true true
true false false true
true unavailable unavailable true
false true false true
false false false false
false unavailable false unavailable
unavailable true unavailable true
unavailable false false unavailable
unavailable unavailable unavailable unavailable

NOT unavailable is unavailable; only true matches a rule. Thus a condition relying on unavailable body data cannot match merely through negation or inequality. An independently true status branch in OR can still match. A decoded error’s test method offers this distinction for a Java predicate over the available DTO.

optionalOn404 handles 404 before the error handler. The compiler rejects contradictory declarative 404 rules; this runtime Java API cannot inspect arbitrary handler code.

Verification and next step

Runtime tests use a loopback HTTP server with no Docker/external service. They cover methods, payloads, 404, status classification, typed rejection, selected metadata, concurrency, response limits, timeouts through body reception, interruption and programmer-error propagation. The Maven execution test compiles and executes the tutorial’s generated adapter, DTOs and mappings against HTTP.

Implemented next layer: a shared resolved REST model, typed request/response bindings, external Java errors and LSP references; see the REST contract. Ordered declarative error rules use this runtime’s truth model; their compiled tutorial includes status-only fallback and unavailable-body tests.

External implementation targets also follow the 0.5.0 shared Java implementation resolution rules. Their mapping/error-handler calling convention is unchanged.