> ## Documentation Index
> Fetch the complete documentation index at: https://pulian.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# @hile/http-over-micro

> Preserve HTTP method, headers, status, JSON bodies, and binary streams across Registry-discovered Hile Micro calls.

# @hile/http-over-micro

Preserve HTTP method, headers, status, JSON bodies, and binary streams across Registry-discovered Hile Micro calls.

## Choose This Package When

| User asks for                               | Use                     | Also read                                                                            |
| ------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
| Preserve HTTP semantics across a Micro call | `@hile/http-over-micro` | [HTTP over Micro](/packages/http-over-micro), [Messaging and Micro](/packages/micro) |

## Use When

Use this package when one HTTP ingress must project a request to a Registry-discovered Hile microservice while preserving HTTP method, duplicate headers, query values, status, cookies, redirects, JSON bodies, and streamed file bodies.

The package defines the transport contract only. The public listener and its authentication, authorization, route selection, header policy, and final Koa response remain owned by the gateway.

## Do Not Use When

* Do not use it for ordinary in-process HTTP controllers that do not cross a Micro boundary.
* Do not use it as a durable queue or retry layer.
* Do not use it for RSC Flight, MCP, WebSocket upgrades, HTTP trailers, or informational `1xx` responses; those protocols keep their own transports.
* Do not put business logic in the gateway merely because the gateway performs the HTTP projection.

## Install

```bash theme={null}
pnpm add @hile/http-over-micro @hile/micro zod
```

## Imports

```ts theme={null}
import {
  callHttpOverMicro,
  defineHttpOverMicroMessage,
} from '@hile/http-over-micro'
import { z } from 'zod'
```

## Copy-Paste Example

Provider message:

```ts theme={null}
// src/messages/posts/[slug].msg.ts
import { defineHttpOverMicroMessage } from '@hile/http-over-micro'
import { z } from 'zod'

export default defineHttpOverMicroMessage({
  method: 'POST',
  schema: {
    headers: z.object({ authorization: z.string().startsWith('Bearer ') }),
    query: z.object({ draft: z.enum(['true', 'false']).default('false') }),
    params: z.object({ slug: z.string().min(1) }),
    body: z.object({ title: z.string().min(1), content: z.string() }),
  },
}, async ({ request, params, invocation }) => {
  // Call a model here; the example only shows the transport result.
  return {
    status: 201,
    headers: {
      location: `/posts/${params.slug}`,
      'set-cookie': ['flash=created; Path=/; HttpOnly', 'draft=; Max-Age=0; Path=/'],
    },
    body: {
      created: true,
      draft: request.query.draft === 'true',
      requestId: invocation.context.values.requestId,
    },
  }
})
```

Gateway-side call:

```ts theme={null}
const response = await callHttpOverMicro(
  app,
  'cn.zlooks.blog.server',
  '/posts/hello',
  {
    method: ctx.method,
    headers: ctx.headers,
    query: Object.entries(ctx.query).flatMap(([name, value]) =>
      Array.isArray(value) ? value.map(item => [name, item] as const) : [[name, String(value)] as const],
    ),
    body: ctx.request.body,
  },
  { context: executionContext },
)

ctx.status = response.status
for (const [name, value] of response.headers) ctx.append(name, value)
return response.body
```

Pass only gateway-approved end-to-end request headers. The sample assumes a body parser has already produced `ctx.request.body`; use the untouched incoming `Readable` instead for raw uploads.

## More Examples

### Upload and download without a transport switch

The caller supplies a normal JSON value or an `AsyncIterable`, `Uint8Array`, or `ArrayBuffer`. `callHttpOverMicro()` snapshots JSON values into the request envelope and automatically moves binary or iterable bodies into Micro request input.

```ts theme={null}
import { createReadStream } from 'node:fs'

const response = await callHttpOverMicro(app, 'files.server', '/files', {
  method: 'PUT',
  headers: { 'content-type': 'application/octet-stream' },
  body: createReadStream('/tmp/archive.tar'),
}, { context })

if (response.bodyKind === 'stream') {
  for await (const chunk of response.body) consumeDownloadedChunk(chunk)
}
```

Provider:

```ts theme={null}
import { Readable } from 'node:stream'

export default defineHttpOverMicroMessage({ method: ['GET', 'PUT'] }, async ({ request }) => {
  if (request.method === 'PUT') {
    if (!(request.body instanceof Readable)) {
      return { status: 400, body: { error: 'stream required' } }
    }
    await saveUpload(request.body)
    return { status: 204 }
  }

  return {
    status: 200,
    headers: { 'content-type': 'application/octet-stream' },
    body: openDownloadStream(),
  }
})
```

Streaming request bodies are non-replayable. Leave retries unset or set `retries: 0`; an explicit nonzero value fails before dispatch.

### Response metadata before response bytes

Every HTTP-over-Micro call uses one Micro response stream. Its first chunk is a response head; body chunks follow only when `body.kind` is `stream`:

```ts theme={null}
type RequestEnvelope = {
  protocol: '@hile/http-over-micro'
  version: 1
  type: 'request'
  method: string
  headers: Array<[string, string]>
  query: Array<[string, string]>
  body: { kind: 'empty' } | { kind: 'inline'; value: unknown } | { kind: 'stream' }
}

type ResponseHead = {
  protocol: '@hile/http-over-micro'
  version: 1
  type: 'response'
  status: number
  headers: Array<[string, string]>
  body: { kind: 'empty' } | { kind: 'inline'; value: unknown } | { kind: 'stream' }
}
```

Header tuples deliberately preserve order and duplicate fields such as `Set-Cookie`. Header names are canonical lowercase on the wire. Query tuples preserve repeated query values without inventing an object serialization rule.

### Zod parses, not merely checks

`schema.headers`, `schema.query`, `schema.params`, and `schema.body` each receive the logical value seen by the handler. Schema transforms and coercions are returned to the handler.

For a streamed request, `schema.body` receives the `Readable`. Use a Zod custom or union schema only when that endpoint intentionally accepts a stream; a JSON-object body schema rejects the stream naturally.

## Runtime And Lifecycle Notes

* `defineHttpOverMicroMessage()` returns a normal `defineMicroMessage()` definition and is loaded or registered through the standard Micro message loader.
* The Micro route remains the file/message path. HTTP method is protocol data and one definition may accept one method or a method list. Unsupported methods return `405` plus `Allow` without invoking the handler.
* Inline bodies use JSON serialization semantics and default to a 1 MiB bound. Override `limits.maxInlineBodyBytes` explicitly on both ends when a deployment requires another limit; use streams for files and large byte bodies.
* Final response statuses are `200..599`. `HEAD`, `204`, `205`, and `304` responses reject bodies. `1xx`, protocol upgrades, and trailers are intentionally out of scope.
* Destroying the returned streamed body cancels the underlying Micro response. `signal`, total timeout, idle timeout, and stream window are passed to `Application.stream()`.
* Credit-based backpressure bounds buffered chunks, not the total number of transferred bytes. The HTTP ingress and provider handler must each enforce their endpoint-specific upload/download byte limit while consuming a stream.
* The caller owns the returned response stream and must consume or destroy it. The package fully consumes empty and inline responses before resolving.
* Cookies and `Location` are opaque response headers. The public gateway remains responsible for cookie ownership, security attributes, redirect policy, and hop-by-hop header removal.
* Treat `namespace` and `url` as routing authority. A public gateway must resolve them through its validated provider catalog or an equivalent allow policy; never dispatch an arbitrary client-supplied namespace directly.

## Anti-Patterns

* Do not handcraft the request envelope or consume the response-head frame yourself; use `callHttpOverMicro()`.
* Do not call `Application.call()` for this protocol. A response may need headers followed by a body stream, so the package intentionally uses `Application.stream()` for every result.
* Do not encode files as Base64 inside an inline JSON body.
* Do not collapse response headers into a plain object when duplicate `Set-Cookie` values matter.
* Do not enable retries for an upload stream.
* Do not forward every inbound header, raw cookie, or identity credential merely because the protocol can carry it.

## Verification Checklist

* Unit tests cover inline, empty, malformed, method-mismatch, and Zod-coercion cases.
* A real Registry-discovered WebSocket test carries request and response streams at the same time.
* Duplicate response headers survive in order.
* Invalid request metadata fails with HTTP status `400`; invalid upstream response metadata fails locally with `502`.
* Inline bodies are bounded and JSON-serializable; files use stream bodies.
* Cancellation, timeout, idle timeout, and backpressure remain owned by `@hile/micro` and `@hile/message-modem`.

## Related Recipes

### Micro RPC With Message Loader

## Complete Example

Provider handler:

```ts theme={null}
// src/messages/charge.msg.ts
import { defineMicroMessage } from '@hile/micro'

export default defineMicroMessage(async ({ data, invocation }) => {
  return { charged: true, input: data, requestId: invocation.context.values.requestId }
})
```

Provider boot:

```ts theme={null}
// src/services/app.boot.ts
import { defineService } from '@hile/core'
import { Application } from '@hile/micro'

export default defineService('billing.micro', async (shutdown) => {
  const app = new Application({
    namespace: 'billing',
    registry: { host: '127.0.0.1', port: 9876 },
    advertiseHost: '127.0.0.1',
  })

  await app.load(new URL('../messages', import.meta.url).pathname)
  const stop = await app.listen(9101)
  shutdown(stop)
  return app
})
```

Consumer:

```ts theme={null}
import { randomUUID } from 'node:crypto'
import { createExecutionContext } from '@hile/context'

const context = createExecutionContext({ requestId: randomUUID(), tenantId: 't1' })
const result = await app.call('billing', '/charge', {
  tenantId: 't1',
  amount: 100,
}, { context })
```

## Package-Local AI Guide

This package also ships `AI.md` in npm so agents can read accurate examples after installation.
