HTTP

HTTP server and client. Bodies are streamable; every operation that can fail returns Result. See Error Handling.

Status

Function signatures below are the specified interface. The current C runtime returns raw int64_t for the create/listen/stop and request functions; the type system expects Result<T, string> and the bridge is being aligned. The handler bridge in httpListen currently expects a raw string return rather than the HttpResponse record described here.

Types

type HttpMethod = GET | POST | PUT | DELETE | PATCH | HEAD | OPTIONS

type HttpRequest = {
    method:      HttpMethod,
    path:        string,
    headers:     string,
    body:        string,
    queryParams: string
}

type HttpResponse = {
    status:      int,
    headers:     string,
    contentType: string,
    streamFd:    int,      // file descriptor for streaming; -1 if not streaming
    isComplete:  bool,
    partialBody: string    // length computed by the runtime; do not pass an explicit length
}

type ServerID = int
type ClientID = int

Server Functions

httpCreateServer(port: int, address: string) -> ResultServerID, string>

httpListen(
    serverID: ServerID,
    handler: fn(method: string, path: string, headers: string, body: string) -> HttpResponse
) -> Resultunit, string>

httpStopServer(serverID: ServerID) -> Resultunit, string>

The C runtime parses incoming requests and calls handler per request. All routing and application logic lives in Osprey; the C side provides only the transport.

fn handleRequest(method: string, path: string, headers: string, body: string) -> HttpResponse =
    match method {
        "GET" => match path {
            "/health" => jsonResponse(status: 200, body: "{\"status\":\"healthy\"}")
            "/users"  => jsonResponse(status: 200, body: "{\"users\":[...]}")
            _         => jsonResponse(status: 404, body: "{\"error\":\"not found\"}")
        }
        "POST" => match path {
            "/users" => jsonResponse(status: 201, body: "{\"id\":3}")
            _        => jsonResponse(status: 404, body: "{\"error\":\"not found\"}")
        }
        _ => jsonResponse(status: 405, body: "{\"error\":\"method not allowed\"}")
    }

fn jsonResponse(status: int, body: string) -> HttpResponse = HttpResponse {
    status:      status,
    headers:     "Content-Type: application/json",
    contentType: "application/json",
    streamFd:    -1,
    isComplete:  true,
    partialBody: body
}

match httpCreateServer(port: 8080, address: "127.0.0.1") {
    Success { value: serverID } => httpListen(serverID: serverID, handler: handleRequest)
    Error   { message }         => print("create failed: ${message}")
}

Client Functions

httpCreateClient(baseUrl: string, timeout: int) -> ResultClientID, string>

httpGet(   clientID: ClientID, path: string,                            headers: string) -> ResultHttpResponse, string>
httpPost(  clientID: ClientID, path: string, body: string,              headers: string) -> ResultHttpResponse, string>
httpPut(   clientID: ClientID, path: string, body: string,              headers: string) -> ResultHttpResponse, string>
httpDelete(clientID: ClientID, path: string,                            headers: string) -> ResultHttpResponse, string>
httpRequest(clientID: ClientID, method: HttpMethod, path: string,
            headers: string, body: string)                              -> ResultHttpResponse, string>

httpCloseClient(clientID: ClientID) -> Resultunit, string>
match httpCreateClient(baseUrl: "http://api.example.com", timeout: 5000) {
    Success { value: clientID } => match httpGet(clientID: clientID, path: "/users", headers: "") {
        Success { value: response } => print("status: ${response.status}")
        Error   { message }         => print("request failed: ${message}")
    }
    Error { message } => print("client create failed: ${message}")
}