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:
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 (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#
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 gourl— an absolutehttp/httpsURL.body/contentType(Post/Put/Patch) — the request body and its media type (e.g."application/json"). The body may be astring, aJsonvalue or abyte[]; 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
Jsonvalue, soHttp.Post(url, new { total = 12.5m }, "application/json")is the shortest correct spelling. ⛔ Do not wrap it inJsonSerializer.Serialize— aJsonvalue 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.Serializeis for the shapes that are not already JSON: aclass, a scalar, a list, a map. headers(optional, any verb) — extra request headers such asAuthorization. ADictionary<string, string>.
Reading a response as it arrives#
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:
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#
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.
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#
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[].
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.GetToFileanswers theFileAssetit 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.PutFilesends the asset's current content and answers the ordinaryHttpResponse, so a non-2xx is data here exactly as it is for the other verbs. The asset's ownMimeTypeis 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#
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:
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#
Http.* is enabled by declaring the dependency in your app manifest:
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:
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#
Post a JSON webhook and record whether it was accepted:
// `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:
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#
- HttpResponse — the
StatusCode/Body/IsSuccessresult every verb returns - Outbound calls in a test — a
[Test]makes the call for real;Http.<Verb>.Stub(url => …)answers it instead - File.Url — the other capability-gated I/O surface (
use Osysharp.Storage;)