# HTTP functions
Source: https://sovaria.in/docs/functions-http

Answer any method on any sub-path with your own status, headers and body: webhooks that verify signatures on the raw body, OAuth callbacks, GET APIs and files, with Web Request/Response and Deno.serve.

Every function answers `/functions/v1/<name>` and every path below it (`/functions/v1/<name>/users/42`), on every method. How the request reaches your code depends on the function's mode.

| Mode | Your handler gets | It returns |
|---|---|---|
| **JSON** (default) | The JSON body as the event; `context.http` has the method, sub-path, query and headers | A value, sent back as JSON with status 200 |
| **HTTP** | The whole request: method, sub-path, query, headers, **raw body** (bytes), client IP | A response: status, headers and any body (text, JSON, binary, a redirect) |

(Diagram: An HTTP call to a function)

## Make a function an HTTP function

- Write a handler that is HTTP by itself: Python `def fetch(request)`, JavaScript/TypeScript `export default { fetch(req, ctx) {} }` or `Deno.serve(handler)`.
- Or keep `handler` and turn HTTP mode on: console › Functions › your function › **Who can call it** › "HTTP function", `sov fn deploy --http`, or `PUT /api/v1/fn/functions/<name>/http {"http": true}`.

index.ts (Bun) / index.mjs (Node):
```ts
export default {
  async fetch(req: Request, ctx) {               // ctx.path = the sub-path, ctx.caller = who called
    const url = new URL(req.url)
    if (req.method === 'GET' && ctx.path.startsWith('/users/')) {
      return Response.json({ id: ctx.path.split('/')[2], q: url.searchParams.get('q') })
    }
    ctx.waitUntil(fetch('https://hooks.example.com/seen', { method: 'POST' }))   // finishes before the call ends
    return new Response('Not found', { status: 404 })
  },
}
```

main.py:
```python
from sovaria_functions import Response

def handler(request, context):
    # request.method, .path, .url, .query, .headers (case-insensitive), .body (bytes), .text(), .json(), .form(), .ip
    if request.method == "GET" and request.path.startswith("/users/"):
        return Response.json({"id": request.path.split("/")[2], "q": request.query.get("q")})
    if request.path == "/logo.png":
        return Response(open("logo.png", "rb").read(), headers={"content-type": "image/png"})
    return Response("Not found", status=404)      # also (body, status, headers) or Response.redirect(url)
```

Supabase-style:
```ts
// Supabase Edge Function code runs as it is: Deno.serve, Deno.env.get and EdgeRuntime.waitUntil work.
Deno.serve(async (req) => {
  const { name } = await req.json()
  return new Response(JSON.stringify({ message: `Hello ${name}!` }), { headers: { 'Content-Type': 'application/json' } })
})
```

Go functions in HTTP mode get the request as the event (`method`, `path`, `query_string`, `headers`, `body_b64`, `ip`) and return `{"statusCode": 200, "headers": {…}, "body": "…", "isBase64Encoded": false}`.

## Verify a webhook signature

Signatures are computed over the exact bytes the provider sent, so verify them on the raw body. Make the function **public** (console › Who can call it, or `PUT …/access {"public": true}`): providers call without a key. Keep the signing secret in the function's secrets.

Stripe (Python):
```python
import hashlib, hmac, os, time
from sovaria_functions import Response

def handler(request):
    parts = dict(p.split("=", 1) for p in request.headers.get("stripe-signature", "").split(",") if "=" in p)
    signed = f"{parts.get('t')}.".encode() + request.body
    expected = hmac.new(os.environ["STRIPE_WEBHOOK_SECRET"].encode(), signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
        return Response("bad signature", status=400)
    event = request.json()
    # … handle event["type"] …
    return Response.json({"received": True})
```

GitHub (TypeScript):
```ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export default {
  async fetch(req: Request) {
    const raw = Buffer.from(await req.arrayBuffer())
    const sent = req.headers.get('x-hub-signature-256') ?? ''
    const ours = 'sha256=' + createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET!).update(raw).digest('hex')
    if (sent.length !== ours.length || !timingSafeEqual(Buffer.from(sent), Buffer.from(ours))) {
      return new Response('bad signature', { status: 401 })
    }
    const event = req.headers.get('x-github-event')
    return Response.json({ ok: true, event })
  },
}
```

## Call it

JavaScript:
```js
const res = await sovaria.functions.fetch('api', 'users/42', { method: 'PUT', body: JSON.stringify(user),
  headers: { 'Content-Type': 'application/json' } })
if (res.status === 201) console.log(await res.json())
```

Python:
```python
res = await FunctionsClient(sov).fetch("api", "users/42", method="PUT", body=b"...")
print(res.status_code, res.headers["content-type"], res.content)
```

Dart / Swift / Kotlin:
```dart
await sovaria.functions.invoke('api', method: 'GET', path: 'users/42', query: {'q': '1'});
```

CLI:
```bash
sov fn call api users/42 -X PUT -H 'Content-Type: application/json' -d '{"name":"Ada"}' -i
sov fn serve ./functions --http api        # locally: any method and sub-path, same contract as production
```

curl:
```bash
curl -X DELETE "https://<project-ref>.sovaria.in/functions/v1/api/users/42" -H "apikey: <anon key>"
```

## What Sovaria adds and removes

- Your Sovaria credentials (`Authorization`, `apikey`, Sovaria cookies) and the gateway's internal headers are never passed to your code. Who called is in `context.caller` (user id, role). A keyless call to a public function keeps its own `Authorization` header (a provider's bearer token).
- Your app's cookies reach the function only on your **custom domain**; on the shared `<ref>.sovaria.in` host the browser also sends cookies for the platform domain, which never reach customer code.
- On the shared project host, HTML (and SVG/XML) responses are sent as `text/plain` with a sandbox CSP, as Supabase does: the console and API live on the same site. Serve pages from [web hosting](https://sovaria.in/docs/hosting) (a rewrite to the function) or a custom domain.
- A response may not set framing headers (`Content-Length`, `Transfer-Encoding`, `Content-Encoding`), `Strict-Transport-Security` or `X-Sovaria-*`; cookies are host-only (no `Domain`) and may not use Sovaria's cookie names.
- The WAF, the function's policy (allowed origins, payload limit, rate limit), the plan's concurrency and the monthly quota apply to every call, whatever the method.
- The gateway waits for your plan's function timeout (`fn.timeout_sec`, plus a small margin), so a 5-minute report on Ultra returns its result instead of a 504.

|  | Free | Pro | Ultra | Team | Enterprise |
|---|---|---|---|---|---|
| Largest request body for an HTTP function call | 1 MB | 6 MB | 10 MB | 20 MB | 20 MB |
| Function timeouts | 10 s | 1 min | 5 min | 15 min | 15 min |
| Concurrent function invocations | 5 | 50 | 200 | 500 | 2,000 |

Responses are up to 10 MB. Bodies above the plan's `fn.http_body_bytes` get `413`. A JSON function called with a body that isn't JSON gets `400` with a hint to switch to HTTP mode.
