Skip to content

Latest commit

 

History

History
668 lines (488 loc) · 17.8 KB

File metadata and controls

668 lines (488 loc) · 17.8 KB

WhatwgAdapter

WhatwgAdapter isolates WHATWG runtime specifics at the boundary so the rest of the application can keep using the same server and handler model.

WHATWG Fetch API adapter that bridges standard Request and Response objects with the Ingest framework, enabling seamless integration with serverless environments and modern web standards.

import WhatwgAdapter from '@stackpress/ingest/whatwg/Adapter';

// Static usage
const response = await WhatwgAdapter.plug(context, request);

// Instance usage
const adapter = new WhatwgAdapter(context, request);
const response = await adapter.plug();
  1. Plugging WHATWG Requests
  2. Methods
  3. Request Processing Flow
  4. Body Loading
  5. Response Dispatching
  6. Serverless Integration Examples
  7. Static Functions
  8. Advanced Usage
  9. Best Practices

1. Plugging WHATWG Requests

The following example shows how to handle WHATWG requests using the static plug method for serverless environments.

import { server } from '@stackpress/ingest/whatwg';

const app = server();

// Basic usage
export default async function handler(request: Request) {
  return await WhatwgAdapter.plug(app, request);
}

// With custom action
export async function customHandler(request: Request) {
  return await WhatwgAdapter.plug(app, request, 'custom-handler');
}

// With action function
export async function functionHandler(request: Request) {
  return await WhatwgAdapter.plug(app, request, async ({ res }) => {
    res.setJSON({ message: 'Custom handler' });
  });
}

Parameters

Parameter Type Description
context WhatwgServer<C> Server context instance
request Request WHATWG Request object
action string|WhatwgAction<C> Custom action name or function (optional)

Returns

A promise that resolves to a WHATWG Response object.

2. Methods

The following methods are available when instantiating a WhatwgAdapter.

2.1. Processing Requests

The following example shows how to process WHATWG requests through the adapter with flexible action handling.

const adapter = new WhatwgAdapter(context, request);

// Process with automatic route detection
const response = await adapter.plug();

// Process with custom action
const response2 = await adapter.plug('user-login');

// Process with action function
const response3 = await adapter.plug(async ({ res }) => {
  const users = await getUsers();
  res.setResults(users);
});

Parameters

Parameter Type Description
action string|WhatwgAction<C> Custom action name or function (optional)

Returns

A promise that resolves to a WHATWG Response object.

2.2. Creating Request Objects

The following example shows how WHATWG requests are converted to Ingest Request objects.

const adapter = new WhatwgAdapter(context, request);
const ingestRequest = adapter.request();

// Access request properties
console.log(ingestRequest.method);     // 'GET', 'POST', etc.
console.log(ingestRequest.url);        // URL object
console.log(ingestRequest.headers);    // Request headers
console.log(ingestRequest.query);      // Query parameters
console.log(ingestRequest.session);    // Session data from cookies

Returns

An Ingest Request object configured for the WHATWG request.

2.3. Creating Response Objects

The following example shows how WHATWG responses are created for Ingest processing.

const adapter = new WhatwgAdapter(context, request);
const ingestResponse = adapter.response();

// Set response data
ingestResponse.setJSON({ message: 'Hello World' });
ingestResponse.setHTML('<h1>Hello World</h1>');
ingestResponse.setError('Not found', {}, [], 404);

// Dispatch to WHATWG response
const whatwgResponse = await ingestResponse.dispatch();

Returns

An Ingest Response object configured for WHATWG output.

3. Request Processing Flow

WhatwgAdapter follows a structured request processing flow for reliable WHATWG request handling.

3.1. Request Initialization

Convert WHATWG Request to Ingest Request with comprehensive data extraction.

// Convert WHATWG Request to Ingest Request
const request = adapter.request();
// - Extracts HTTP method, URL, headers
// - Parses cookies into session data
// - Converts query parameters to nested object
// - Sets up body loader for POST data

3.2. Response Setup

Create Ingest Response for WHATWG Response with proper configuration.

// Create Ingest Response for WHATWG Response
const response = adapter.response();
// - Configures response dispatcher
// - Sets up cookie handling
// - Prepares header management

3.3. Body Loading

Load request body asynchronously with WHATWG-specific handling.

// Load request body asynchronously
await request.load();
// - Reads POST data from the WHATWG request body
// - Parses JSON, URL-encoded, and multipart form bodies
// - Merges parsed data into request.post and request.data

Like the HTTP adapter, plug() calls request.load() before Route.emit(...), so handlers work against the same loaded request shape regardless of runtime.

3.4. Route Processing

Execute route through Route.emit with complete lifecycle management.

// Execute route through Route.emit
await Route.emit(event, request, response, context);
// - Runs request lifecycle (prepare, process, shutdown)
// - Executes route handlers
// - Handles errors and 404s

3.5. Response Dispatch

Create WHATWG Response with proper headers and content.

// Create WHATWG Response
const whatwgResponse = await response.dispatch();
// - Creates new Response object
// - Sets status code and message
// - Writes cookies to Set-Cookie headers
// - Sends response headers and body

4. Body Loading

WhatwgAdapter provides robust body loading for WHATWG requests with native API support.

4.1. Automatic Body Parsing

Parse request bodies automatically based on content type using WHATWG APIs.

// Body is automatically loaded and parsed
await request.load();

// Access parsed data
const formData = request.post.get();     // Form data
const jsonData = request.data.get();     // Combined data
const rawBody = request.body;            // Raw body string

The WHATWG loader follows the same content-type rules as the HTTP loader: JSON, URL-encoded, and multipart inputs are parsed into request.post; unsupported types leave request.post empty while still caching the raw body text.

4.2. Content Type Handling

Handle different content types with appropriate parsing strategies.

// Form data (application/x-www-form-urlencoded)
// Content-Type: application/x-www-form-urlencoded
// Body: name=John&email=john@example.com
// Result: { name: 'John', email: 'john@example.com' }

// JSON data (application/json)
// Content-Type: application/json
// Body: {"name":"John","email":"john@example.com"}
// Result: { name: 'John', email: 'john@example.com' }

// FormData (multipart/form-data)
// Handles file uploads and form fields

4.3. Async Body Loading

Use WHATWG Request body methods for different data types.

// WHATWG Request body loading
const bodyText = await request.text();
const bodyJson = await request.json();
const bodyFormData = await request.formData();
const bodyArrayBuffer = await request.arrayBuffer();

One runtime detail worth knowing is that the WHATWG loader does not currently enforce a request size limit the way the HTTP loader can. If strict body caps matter, that policy still has to live above or around the adapter today.

5. Response Dispatching

WhatwgAdapter handles various response types and formats with WHATWG Response creation.

The dispatch path mirrors the HTTP adapter closely, but the final target is a WHATWG Response. Strings, buffers, Uint8Array, and ReadableStream values are written directly, Node Readable streams are converted, and plain objects or arrays become structured JSON payloads.

If a route or hook already set res.resource to a native WHATWG response, the adapter returns that resource directly instead of rebuilding it. That gives advanced handlers a way to drop down to runtime-native behavior without changing the rest of the application model.

app.get('/native-response', ({ res }) => {
  res.resource = new Response('already built', {
    status: 200,
    headers: { 'Content-Type': 'text/plain' }
  });
});

5.1. Response Type Handling

Handle different response types with appropriate WHATWG Response creation.

// String responses
response.setHTML('<h1>Hello World</h1>');
// Creates Response with text/html content-type

// JSON responses
response.setJSON({ message: 'Success' });
// Creates Response with application/json content-type

// Buffer responses
response.body = Buffer.from('binary data');
// Creates Response with binary data

// Stream responses
response.body = new ReadableStream({...});
// Creates Response with streaming body

// Object responses (automatic JSON)
response.body = { users: [...] };
// Automatically serializes to JSON Response

5.2. Cookie Management

Manage session cookies with automatic serialization for serverless environments.

// Session cookies are automatically handled
response.session.set('user_id', '123');
response.session.set('preferences', JSON.stringify(prefs));

// Cookies are written as Set-Cookie headers
// Set-Cookie: user_id=123; Path=/
// Set-Cookie: preferences={"theme":"dark"}; Path=/

5.3. Header Management

Set custom headers for security, caching, and API versioning.

// Custom headers
response.headers.set('X-API-Version', '1.0');
response.headers.set('Cache-Control', 'no-cache');

// Security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');

6. Serverless Integration Examples

The following examples demonstrate WhatwgAdapter integration with popular serverless platforms.

6.1. Vercel Functions

// api/users.ts
import { server } from '@stackpress/ingest/whatwg';
import WhatwgAdapter from '@stackpress/ingest/whatwg/Adapter';

const app = server();

app.get('/api/users', async ({ res }) => {
  const users = await getUsers();
  res.setResults(users);
});

export default async function handler(request: Request) {
  return await WhatwgAdapter.plug(app, request);
}

async function getUsers() {
  return [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
}

6.2. Netlify Functions

// netlify/functions/api.ts
import { server } from '@stackpress/ingest/whatwg';
import WhatwgAdapter from '@stackpress/ingest/whatwg/Adapter';

const app = server();

app.all('/*', async ({ req, res }) => {
  // Handle all routes
  const path = req.url.pathname;
  res.setJSON({ path, method: req.method });
});

export default async function handler(request: Request) {
  return await WhatwgAdapter.plug(app, request);
}

6.3. Cloudflare Workers

// worker.ts
import { server } from '@stackpress/ingest/whatwg';
import WhatwgAdapter from '@stackpress/ingest/whatwg/Adapter';

const app = server();

app.get('/', async ({ res }) => {
  res.setHTML('<h1>Hello from Cloudflare Workers!</h1>');
});

export default {
  async fetch(request: Request): Promise<Response> {
    return await WhatwgAdapter.plug(app, request);
  }
};

6.4. Deno Deploy

// main.ts
import { server } from '@stackpress/ingest/whatwg';
import WhatwgAdapter from '@stackpress/ingest/whatwg/Adapter';

const app = server();

app.get('/api/hello', async ({ res }) => {
  res.setJSON({ message: 'Hello from Deno!' });
});

Deno.serve(async (request: Request) => {
  return await WhatwgAdapter.plug(app, request);
});

6.5. AWS Lambda (with Response Streaming)

// lambda.ts
import { server } from '@stackpress/ingest/whatwg';
import WhatwgAdapter from '@stackpress/ingest/whatwg/Adapter';

const app = server();

app.get('/api/stream', async ({ res }) => {
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue('chunk 1\n');
      controller.enqueue('chunk 2\n');
      controller.close();
    }
  });
  res.body = stream;
});

export const handler = async (event: any) => {
  const request = new Request(event.requestContext.http.sourceIp, {
    method: event.requestContext.http.method,
    headers: event.headers,
    body: event.body
  });
  
  return await WhatwgAdapter.plug(app, request);
};

7. Static Functions

WhatwgAdapter provides utility functions for request and response handling with WHATWG-specific implementations.

7.1. Request Body Loader

The following example shows how to create a custom body loader for WHATWG requests.

import { loader } from '@stackpress/ingest/whatwg/Adapter';

// Create loader for WHATWG Request
const bodyLoader = loader(request);

// Use with Ingest request
ingestRequest.loader = bodyLoader;
await ingestRequest.load();

Parameters

Parameter Type Description
resource Request WHATWG Request object

Returns

A loader function that reads and parses the request body.

7.2. Response Dispatcher

The following example shows how to create a custom response dispatcher for WHATWG responses.

import { dispatcher } from '@stackpress/ingest/whatwg/Adapter';

// Create dispatcher with cookie options
const responseDispatcher = dispatcher({
  path: '/',
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

// Use with response
response.dispatcher = responseDispatcher;
const whatwgResponse = await response.dispatch();

Parameters

Parameter Type Description
options CookieOptions Cookie configuration options

Returns

A dispatcher function that creates a WHATWG Response object.

8. Advanced Usage

8.1. Custom Request Processing

const app = server();

// Custom request preprocessing
app.on('request', async (req, res) => {
  // Add request ID
  req.data.set('requestId', crypto.randomUUID());
  
  // Parse custom headers
  const apiKey = req.headers.get('x-api-key');
  if (apiKey) {
    req.data.set('apiKey', apiKey);
  }
  
  return true;
});

// Route with custom processing
app.get('/api/data', async ({ req, res }) => {
  const requestId = req.data('requestId');
  const apiKey = req.data('apiKey');
  
  res.setJSON({ requestId, authenticated: !!apiKey });
});

8.2. Stream Processing

app.get('/api/stream', async ({ res }) => {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        controller.enqueue(`data chunk ${i}\n`);
        await new Promise(resolve => setTimeout(resolve, 100));
      }
      controller.close();
    }
  });
  
  res.body = stream;
  res.headers.set('Content-Type', 'text/plain');
  res.headers.set('Transfer-Encoding', 'chunked');
});

8.3. Error Handling

app.on('error', async ({ req, res }) => {
  // Custom error formatting for APIs
  if (req.url.pathname.startsWith('/api/')) {
    res.setJSON({
      error: res.error,
      code: res.code,
      timestamp: new Date().toISOString(),
      path: req.url.pathname
    });
  } else {
    // HTML error pages for web routes
    res.setHTML(`
      <h1>Error ${res.code}</h1>
      <p>${res.error}</p>
    `);
  }
  
  return true;
});

9. Best Practices

The following best practices ensure optimal performance and security in serverless environments.

9.1. Environment Detection

Detect and adapt to different serverless environments for optimal performance.

const app = server();

// Detect serverless environment
app.on('request', async ({ req, res }) => {
  const isVercel = process.env.VERCEL === '1';
  const isNetlify = process.env.NETLIFY === 'true';
  const isCloudflare = typeof caches !== 'undefined';
  
  req.data.set('environment', {
    isVercel,
    isNetlify,
    isCloudflare,
    isServerless: isVercel || isNetlify || isCloudflare
  });
  
  return true;
});

9.2. CORS Handling

Implement proper CORS handling for cross-origin requests.

app.on('request', async ({ req, res }) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    res.headers.set('Access-Control-Allow-Origin', '*');
    res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.headers.set('Access-Control-Max-Age', '86400');
    res.code = 204;
    return false; // Skip further processing
  }
  
  // Set CORS headers for all requests
  res.headers.set('Access-Control-Allow-Origin', '*');
  return true;
});

9.3. Performance Optimization

Optimize responses for serverless environments with appropriate caching.

app.on('response', async ({ req, res }) => {
  // Add caching headers
  if (req.method === 'GET' && res.code === 200) {
    res.headers.set('Cache-Control', 'public, max-age=3600');
    res.headers.set('ETag', `"${Date.now()}"`);
  }
  
  // Compression hint
  res.headers.set('Vary', 'Accept-Encoding');
  
  return true;
});

9.4. Security Headers

Implement security headers appropriate for serverless deployments.

app.on('response', async (req, res) => {
  // Security headers for web responses
  if (!req.url.pathname.startsWith('/api/')) {
    res.headers.set('X-Frame-Options', 'DENY');
    res.headers.set('X-Content-Type-Options', 'nosniff');
    res.headers.set('X-XSS-Protection', '1; mode=block');
    res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  }
  
  return true;
});