How Server Error Envelopes Are Structured and Validated in Instatic

Instatic wraps every failed HTTP response in a TypeBox-validated JSON envelope with an optional error field, extracting human-readable messages through readEnvelope and responseErrorMessage utilities in src/core/http/apiClient.ts.

Instatic implements a consistent, type-safe approach to HTTP error handling using server error envelopes—minimal JSON structures that standardize failure responses across the application. Every API client in the repository relies on a single TypeBox schema defined in src/core/http/apiClient.ts to parse and validate these envelopes. This design ensures that UI components receive predictable error messages while maintaining strict runtime type safety.

Error Envelope Structure and Schema Definition

The canonical envelope shape follows a simple contract: an optional error property that may contain any JSON value, though typically a descriptive string. In src/core/http/apiClient.ts, the TypeBox schema definition looks like this:

const ErrorEnvelopeSchema = Type.Object({
  error: Type.Optional(Type.Unknown()),
})

This schema permits flexibility in error payloads while providing compile-time and runtime validation. The Type.Optional wrapper ensures that responses missing the error field still pass validation, allowing the system to gracefully handle malformed or unexpected server responses.

The Validation Pipeline with readEnvelope

The core validation logic resides in the readEnvelope function, which clones the Response, parses the JSON body, and validates it against the supplied schema. If validation fails or the response is not OK, the utility throws an ApiError containing the extracted message.

The function signature follows this pattern:

readEnvelope<T>(res: Response, Schema: TSchema, fallbackMessage: string): Promise<Static<T>>

When processing error responses, callers pass ErrorEnvelopeSchema to readEnvelope. The utility parses the body via parseJsonResponse, validates the structure against the TypeBox schema, and either returns the typed payload or throws an ApiError initialized with the fallback message.

Extracting Human-Readable Messages

For UI-level error handling, Instatic provides responseErrorMessage, a specialized helper that guarantees a string output regardless of the envelope's contents. Located alongside readEnvelope in src/core/http/apiClient.ts, this function implements the following fallback hierarchy:

  • Extract the error property if present and valid
  • Fall back to the raw response text body
  • Use the provided fallback string if all else fails

This ensures that toast notifications and error boundaries always receive a usable string rather than undefined or complex objects.

Practical Implementation Examples

The following patterns demonstrate how server error envelopes integrate into real Instatic workflows.

Automatic Error Handling in API Requests

Higher-level utilities like apiRequest abstract envelope parsing entirely. When calling endpoints, developers supply a success schema while the client automatically handles error envelopes:

import { apiRequest } from '@core/http'
import { UserSchema } from '@/src/core/persistence/cmsUsers'

async function loadCurrentUser() {
  // Returns Static<typeof UserSchema> on success
  // Throws ApiError with envelope message on HTTP error
  return await apiRequest('/admin/api/cms/user', {
    schema: UserSchema,
  })
}

Manual Envelope Parsing

For low-level fetch operations, manually invoke readEnvelope and responseErrorMessage:

import { readEnvelope, responseErrorMessage, ApiError } from '@core/http'

async function fetchWithRawError(url: string) {
  const res = await fetch(url, { credentials: 'include' })

  if (!res.ok) {
    const msg = await responseErrorMessage(res, `Request failed: ${res.status}`)
    throw new ApiError(msg, res.status)
  }

  return await readEnvelope(res, SomeSuccessSchema, 'Unable to decode response')
}

Unit Testing Error Scenarios

The test suite in src/__tests__/http/apiClient.test.ts validates envelope extraction and fallback behavior:

import { jsonResponse, responseErrorMessage } from '@core/http'

test('responseErrorMessage extracts envelope error', async () => {
  const errRes = jsonResponse({ error: 'boom' }, 500)
  expect(await responseErrorMessage(errRes, 'fallback')).toBe('boom')
})

Summary

  • Server error envelopes in Instatic follow a minimal JSON structure with an optional error field defined by ErrorEnvelopeSchema in src/core/http/apiClient.ts.
  • TypeBox validation occurs through the readEnvelope utility, which parses responses and throws ApiError instances for invalid or failed requests.
  • Message extraction is handled by responseErrorMessage, ensuring UI components always receive string-based error descriptions through a reliable fallback chain.
  • Test coverage in src/__tests__/http/apiClient.test.ts guarantees envelope parsing resilience against malformed JSON or missing bodies.

Frequently Asked Questions

What is the exact TypeBox schema for Instatic's server error envelopes?

The schema is defined in src/core/http/apiClient.ts as Type.Object({ error: Type.Optional(Type.Unknown()) }), allowing an optional error property containing any JSON value while maintaining runtime type safety through TypeBox validation.

How does Instatic handle malformed error responses that don't match the envelope schema?

When readEnvelope encounters invalid JSON or a body missing the expected structure, it throws an ApiError initialized with the provided fallback message. The responseErrorMessage utility provides additional resilience by attempting to read raw response text before falling back to the default string.

Where can I find real-world usage examples of error envelope handling?

Production implementations appear throughout src/core/persistence/ (such as cmsMedia.ts), where persistence layers call readEnvelope with domain-specific success schemas. The comprehensive test suite in src/__tests__/http/apiClient.test.ts demonstrates edge cases and validation behavior.

Is the server error envelope pattern documented outside the source code?

Yes, the architectural rationale and usage guidelines are documented in docs/reference/typebox-patterns.md under the "Server error envelope" section, which explains the design decisions behind the optional error field and TypeBox integration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →