# Http.*

> Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint. `Http.Get`/`Post`/`Put`/`Patch`/`Delete` return an `HttpResponse` you branch on (`StatusCode`, `Body`, `IsSuccess`); a 4xx/5xx is data, not an exception. `Http.Stream`/`StreamEvents` read a response as it arrives instead of in one piece — for server-sent events, NDJSON and anything else that is long or unending. Default-open to public hosts; internal addresses are blocked.

<!-- id: http-facade · area: http · stability: stable · html: https://osysharp.com/reference/http/facade/ -->

## Summary        {#summary}
**`Http.*`** is the outbound-HTTP facade for URLs you can't name at author time — a webhook target, a REST API, an
endpoint that comes from data. It mirrors the shape of a familiar HTTP client:

```osy syntax
use Osysharp.Http;

var r = Http.Get("https://api.example.com/orders/" + order.Code);
if (r.IsSuccess) {
  order.Tracking = r.Body;
}
```

Every verb returns an [HttpResponse](https://osysharp.com/reference/http/response/) (`StatusCode`, `Body`, `IsSuccess`). A non-2xx status is a **normal return**,
not an error — a `404` gives you `r.StatusCode == 404` and `r.IsSuccess == false`, so you branch on the result instead
of catching an exception.

For a **declared, fixed** endpoint with typed request/response, use a `client` block instead — `Http.*` is the
escape hatch for the dynamic case beside it.

## Signature      {#signature}
```osy syntax
use Osysharp.Http;

HttpResponse Http.Get(string url [, Dictionary<string, string> headers])
HttpResponse Http.Delete(string url [, Dictionary<string, string> headers])
HttpResponse Http.Post(string url, string|Json|byte[] body, string contentType [, Dictionary<string, string> headers])
HttpResponse Http.Put(string url, string|Json|byte[] body, string contentType [, Dictionary<string, string> headers])
HttpResponse Http.Patch(string url, string|Json|byte[] body, string contentType [, Dictionary<string, string> headers])

stream<string> Http.Stream(string url [, Dictionary<string, string> headers])         // one frame per LINE
stream<string> Http.StreamEvents(string url [, Dictionary<string, string> headers])   // one frame per server-sent EVENT
stream<string> Http.Stream(string url, string|Json|byte[] body, string contentType [, Dictionary<string, string> headers])
stream<string> Http.StreamEvents(string url, string|Json|byte[] body, string contentType [, Dictionary<string, string> headers])

FileAsset    Http.GetToFile(string url, string name, string mimeType [, Dictionary<string, string> headers])
HttpResponse Http.PutFile(string url, FileAsset asset [, string contentType] [, Dictionary<string, string> headers])

string       Http.Refusal(string url)   // sends nothing: why a call to `url` would be refused, "" when it would go
```

- **`url`** — an absolute `http`/`https` URL.
- **`body`** / **`contentType`** (Post/Put/Patch) — the request body and its media type (e.g. `"application/json"`).
  The body may be a **`string`**, a **`Json`** value or a **`byte[]`**; the argument's type decides how it goes on
  the wire. Bytes go as bytes — pushing them through a string would UTF-8-encode them, which silently corrupts
  anything that is not text.
- An **anonymous object literal is a `Json` value**, so `Http.Post(url, new { total = 12.5m }, "application/json")`
  is the shortest correct spelling. ⛔ Do not wrap it in `JsonSerializer.Serialize` — a `Json` value is already its
  own text, so that encodes the document a second time and the far end receives a JSON *string* of escaped JSON.
  The compiler refuses that call. `Serialize` is for the shapes that are not already JSON: a `class`, a scalar, a
  list, a map.
- **`headers`** (optional, any verb) — extra request headers such as `Authorization`. A `Dictionary<string, string>`.

## Reading a response as it arrives   {#streaming}

The five verbs above wait for the whole response and hand it over at once. That is right for an API call and wrong
for a response that is *long* or has no end: a server-sent-event feed, a line-delimited JSON stream, a log tail, a
model writing an answer a token at a time.

`Http.Stream` and `Http.StreamEvents` read the body **as it arrives**. They answer a `stream<string>`, so the only
place the value can go is a `foreach` inside a `stream<T>` function — which is what keeps the promise honest:

```osy title="forward a server-sent event feed" syntax
use Osysharp.Http;

stream<string> Prices() {
  foreach (var event in Http.StreamEvents("https://feed.example.com/prices")) {
    yield return event;
  }
}
```

### Streaming a request you had to SEND   {#streaming-post}

Give either verb a **body** and it POSTs — which is how every streaming model API works: the conversation goes up in
the request, and the answer comes back as events.

```osy title="ask a model and read its answer as it is written" syntax
use Osysharp.Http;

stream<string> Answer(string question) {
  var headers = new Dictionary<string, string>();
  headers.Add("accept", "text/event-stream");
  foreach (var e in Http.StreamEvents("https://api.example.com/v1/messages",
                                      new { model = "m", stream = true, prompt = question },
                                      "application/json", headers)) {
    yield return e;
  }
}
```

There is no `method` argument: a body means POST, and the rest of this family names its method in the verb. The
arguments are in the same order as `Http.Post`'s.

**Which verb**: `Http.Stream` gives you one frame per **line** — NDJSON, a log tail, most chunked APIs.
`Http.StreamEvents` gives you one frame per **server-sent event**: the `data:` payload, with multi-line data joined
by newlines. Heartbeat comments, `event:` and `id:` are consumed for you and never appear as frames.

⚠ **Two verbs rather than one that reads `Content-Type`.** What a frame *is* is something you reason about while
writing the loop, so it is yours to say — not the server's to decide for you a call later.

The two host limits mean something different here, and both differences matter:

| | the five buffered verbs | `Stream` / `StreamEvents` |
|---|---|---|
| size | caps the whole response | caps **one frame** — a feed is meant to be long; what must not happen is a frame with no end in sight |
| timeout | caps the whole call | an **idle** timeout — a quiet healthy stream is not a hung one, and the clock resets on every frame |

⛔ **A 4xx/5xx raises here rather than being data.** Once you are iterating there is no status left to branch on, so
a failing request has to stop you rather than hand you an error page as the first frame.

## Moving a file, without holding it   {#files}

The other kind of big response is one you want *whole* — an export, an object from a blob store, a result chunk a
warehouse hands back. `Http.GetToFile` downloads straight **into** a file and `Http.PutFile` uploads one straight
**out** of it; neither ever holds the content as a `byte[]`.

```osy title="fetch a chunk, then push it somewhere else" syntax
use Osysharp.Http;
use Osysharp.Storage;

void Archive(string chunkUrl) {
  var chunk = Http.GetToFile(chunkUrl, "chunk.csv", "text/csv");
  Http.PutFile("https://archive.example.com/objects/chunk.csv", chunk);
}
```

That is the difference from `Http.Get(url).Bytes`, and it is not convenience: the buffered read is capped at the
platform's whole-response limit and hands you an array that has to fit in memory. These two are bounded by your
app's **storage budget** instead — the same budget every other file is charged against.

- **`Http.GetToFile`** answers the [`FileAsset`](https://osysharp.com/reference/storage/file-url/) it created, ready to attach to a record.
  ⛔ A 4xx/5xx **writes nothing and raises**: you asked for a file, and an asset holding an error page is worse than
  any failure.
- **`Http.PutFile`** sends the asset's current content and answers the ordinary `HttpResponse`, so a non-2xx is data
  here exactly as it is for the other verbs. The asset's own `MimeType` is the content type unless you name one.

⚠ Both need **`use Osysharp.Storage;`** as well as `use Osysharp.Http;` — they are the two verbs that touch both.

## Asking before you send   {#refusal}

`Http.Refusal(url)` sends nothing. It answers the sentence a call from this code to `url` **would** be refused with,
or `""` when the call would go:

```osy title="refuse an address when a person types it, not at the first send" syntax
string CheckedEndpoint(string url) {
  var refused = Http.Refusal(url);
  if (refused != "") { throw new ValidationException(refused); }
  return url;
}
```

The answer comes from the same rules the send obeys: what your app granted a package (`use … { egress … }`), your
app's own allowed and denied hosts, and your **organization's** allowed hosts, which bound everything else, `egress
"*"` included. So a kit that takes addresses from people, such as an endpoint for outbound webhooks, can refuse one
outside the organization's list while the person is still typing it. The sentence names the host and where to allow
it: *"…Add `hooks.example.com` to your organization's allowed hosts if this app should reach it."*

It is a question, not a permit. The send asks again, so a host taken off a list after the check is still refused.
It does not resolve the address, so the internal-address guard (loopback, private and metadata addresses) is applied
only when you send.

In a test the rules are an operator's, so pin the answer instead:
`Http.Refusal.Stub(url => url.StartsWith("https://hooks.example.test/") ? "" : "…not allowed…");`.

## Description    {#description}
`Http.*` is enabled by declaring the dependency in your app manifest:

```osy title="the manifest declaration that enables Http.*" syntax
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}
```

An `Http.*` call without `use Osysharp.Http;` is a compile error naming the fix — a network dependency is visible in the
manifest, not hidden in a function body.

**Default open, host-protected.** You can reach any **public** host — that's your call, the same as any dependency.
What the platform guarantees is that you **cannot** reach its own internals: a URL that resolves to a loopback,
private (`10.x`/`192.168.x`/…), or cloud-metadata address is refused — checked against the *resolved* address, so a
hostname that points at an internal IP is blocked too. Two more limits protect the host: an absolute **timeout ceiling**
and a **maximum response size**; a call that runs too long is cancelled and an over-size response is refused.

**Headers and auth.** Pass a headers map to authenticate an outbound call or set a custom content type:

```osy title="authenticating an outbound call with a headers map" syntax
var headers = new Dictionary<string, string>();
headers.Add("Authorization", "Bearer " + token);
var r = Http.Post("https://hooks.example.com/notify", payload, "application/json", headers);
```

**Bodies are text.** The request `body` and the response `Body` are strings. Build or parse JSON with the JSON surface
(paired with this facade) — `Http.*` moves the bytes; it doesn't assume a format.

**Long-running callbacks aren't held connections.** If an external system calls you back minutes or hours later, model
that as a workflow event (a webhook that raises an event), not an `Http.*` call that blocks — the timeout ceiling
exists precisely so a call can't hold a connection open indefinitely.

## Examples       {#examples}

Post a JSON webhook and record whether it was accepted:

```osy title="post a JSON webhook" test app=http-facade
// `use` is a MANIFEST declaration — it belongs in your app.osy, not in a model file.
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}

entity Order {
  [Required] string Code;
  bool WebhookAccepted;
  int WebhookStatus;
}

void NotifyShipped(Order order) {
  var body = "{\"order\":\"" + order.Code + "\",\"status\":\"shipped\"}";
  var r = Http.Post("https://hooks.partner.com/orders", body, "application/json");
  order.WebhookAccepted = r.IsSuccess;
  order.WebhookStatus = r.StatusCode;
}
```

Call an authenticated API and use the response body:

```osy title="send request headers, and read the response body" test app=http-facade
string LookupTracking(string carrier, string code, string token) {
  var headers = new Dictionary<string, string>();
  headers.Add("Authorization", "Bearer " + token);
  var r = Http.Get("https://api." + carrier + ".com/track/" + code, headers);
  return r.IsSuccess ? r.Body : "";
}
```

## See also       {#see-also}
- [HttpResponse](https://osysharp.com/reference/http/response/) — the `StatusCode` / `Body` / `IsSuccess` result every verb returns
- [Outbound calls in a test](https://osysharp.com/reference/testing/outbound-calls/) — a `[Test]` makes the call for real; `Http.<Verb>.Stub(url => …)` answers it instead
- [File.Url](https://osysharp.com/reference/storage/file-url/) — the other capability-gated I/O surface (`use Osysharp.Storage;`)
