# Request and response

> Reference for request and response on ctx, covering validated input, headers, raw body access, status codes, errors, middleware state, and triggers.

HTTP-triggered functions read their request through `ctx.input` (validated, merged input), `ctx.method`, `ctx.headers`, `ctx.path`, and `ctx.request` (raw body access). They respond by returning a value, setting `ctx.status()`, or throwing `ctx.error()`. Non-HTTP function types receive their payload on `ctx.trigger` instead.

## ctx.input

`ctx.input` is one object merging path params, query params, and the request body. When the file has a named export `input` (a Zod schema), the runtime validates the merged object before the handler runs and returns 400 on failure; the generic `defineFunction<typeof input>` types `ctx.input` from the same schema. With no schema, `ctx.input` holds the raw merged object, unvalidated.

Merge priority on key conflicts: **path params > body > query params**. Path params always win because the URL structure defines them.

| Source | Example |
|---|---|
| Path params | `{ userId: "abc-123" }` |
| Query params | `{ page: "2", limit: "10" }` |
| Request body | `{ name: "Alice" }` |
| Merged, then validated | `{ userId: "abc-123", page: 2, limit: 10, name: "Alice" }` |

Query params arrive as strings; use `z.coerce.number()` for numeric fields. A named `output` export validates the return value after the handler completes (500 on failure); it is skipped for streaming responses.

## ctx.method, ctx.headers, ctx.path

| Field | Type | Description |
|---|---|---|
| `method` | `string` | The HTTP method. Branch on it in catch-all files (no method suffix). |
| `headers` | `Headers` | Standard web `Headers`; read with `ctx.headers.get('x-api-key')`. |
| `path` | `string` | The request path. |

## ctx.request

Raw request access for advanced handling:

```typescript
request: {
  isMultipart: boolean;                    // Content-Type is multipart/form-data
  isRaw: boolean;                          // body is not JSON or form data
  files: () => Promise<UploadedFile[]>;    // parsed multipart file parts
  buffer: () => Promise<ArrayBuffer>;      // whole body as bytes
  text: () => Promise<string>;             // whole body as text
  signal: AbortSignal;                     // fires when the client disconnects
}
```

Each uploaded file exposes its form `fieldname`, `buffer`, `mimetype`, `size`, and `extension`, so a handler can read an upload in memory and act on it. Helix ships no file storage to persist it to: `ctx.files` is platform design. `signal` is the cleanup hook for long-lived streams; see [the streaming guide](/documentation/guides/streaming/).

## ctx.status() and ctx.error()

```typescript
status(code: number): void
error(status: number, message: string): HttpError
```

`ctx.status(code)` sets the response status for the returned value (for example 201 after a create, or 204 with no return value). `ctx.error(status, message)` builds an `HttpError` for you to throw; the runtime turns it into the corresponding HTTP response.

## ctx.state

`ctx.state` is a `Record<string, any>` for middleware-to-handler handoff. Middleware in `functions/_middleware.ts` (or a nested `_middleware.ts`) writes to it before calling `next()`; the handler reads it. See [the functions and routing guide](/documentation/guides/functions-and-routing/) for middleware ordering.

```typescript
// functions/api/v1/_middleware.ts sets ctx.state.apiKey; the handler reads it:
const apiKey = ctx.state.apiKey;
```

## ctx.trigger by function type

`defineFunction` is the only function type in the shipped product, because HTTP is the only trigger. The other three rows describe platform design.

| Function type | `ctx.trigger` contents |
|---|---|
| `defineFunction` | Request data lives on `ctx.input`, `ctx.method`, `ctx.headers`, and `ctx.request`; `trigger.data` and `trigger.scheduledAt` are unavailable. |
| `defineSchedule` | `{ type: 'schedule', scheduledAt, firedAt, isRetry, isManual }`. `scheduledAt` is when the cron said to fire, `firedAt` when it actually ran, `isRetry` marks a retried execution, `isManual` marks a `helix trigger` run. |
| `defineAppTrigger` | `{ type: 'app', data }` where `data` is the webhook payload, typed from the connector catalog via `helix generate-types`. |
| `defineQueueConsumer` | No trigger data; the payload is `ctx.input` and delivery metadata is `ctx.message`. |

## Returning objects vs Response

Return a plain object and the runtime wraps it as JSON with status 200 (or whatever `ctx.status()` set). Return a `Response` for full control over status, headers, and body, including streaming bodies (`ReadableStream`), which work locally and in production via Lambda response streaming.

```typescript
// functions/api/v1/users/[userId].put.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';

export const input = z.object({
  userId: z.string().uuid(),              // from path param [userId]
  name: z.string().min(1).optional(),     // from request body
  email: z.string().email().optional(),   // from request body
});

export default defineFunction<typeof input>(async (ctx) => {
  const { userId, ...updates } = ctx.input;
  const kv = ctx.kv();

  const user = await kv.getItem<Record<string, unknown>>(`user:${userId}`);
  if (!user) throw ctx.error(404, 'User not found');

  const updated = { ...user, ...updates };
  await kv.setItem(`user:${userId}`, updated);

  return updated; // 200 OK, JSON
});
```

And with a `Response` for a non-JSON body:

```typescript
return new Response(csv, {
  status: 200,
  headers: {
    'Content-Type': 'text/csv',
    'Content-Disposition': 'attachment; filename="report.csv"',
  },
});
```

SSE patterns, transform streams, and platform limits for streamed responses are covered in [the streaming guide](/documentation/guides/streaming/).

---

Canonical: https://helix.tray.ai/documentation/reference/context/request/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt