Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

Client.pudu

Pudu198 lines9.5 KB

GitHub ↗
1/** @HttpClient.Client.Module — a configured client sending requests through its pipeline */2module PuduLangHttpClient.Client34import Std.Concurrent.Cancel as Cancel5import Std.Http as Http6import Std.Option as Option7import PuduLangHttpClient.Constants.Defaults as Defaults8import PuduLangHttpClient.Constants.Messages as Messages9import PuduLangHttpClient.Content as Content10import PuduLangHttpClient.Domain.Headers as Headers11import PuduLangHttpClient.Domain.Uri as Uri12import PuduLangHttpClient.Domain.Version as Version13import PuduLangHttpClient.Handler as Handler14import PuduLangHttpClient as HttpClient15import PuduLangHttpClient.Options as RequestOptions16import PuduLangHttpClient.Request as Request17import PuduLangHttpClient.Response as Response18import PuduLangHttpClient.Transport as Transport19import PuduLangHttpClient.Utils.Shared as Shared20import PuduLangHttpClient.Utils.Template as Template21import PuduLangHttpClient.Utils.Tokens as Tokens2223/** @HttpClient.Client.Client — a pipeline and the defaults every request through it gets */24export type Client = {25  name: Str,26  send: Handler.Send,27  baseAddress: Option[Str],28  defaultHeaders: Array[(Str, Str)],29  timeout: Int,30  maxResponseContentBufferSize: Int,31  defaultVersion: Http.Version,32  defaultVersionPolicy: Version.Policy,33  pending: Shared.Shared[Cancel.Token]34}3536/// The reason given to requests cancelled by `cancelPending`.37const PENDING_CANCELLED: Str = "the client cancelled its pending requests"3839/// A client sending through `send`, with no base address or default headers, a 100-second timeout,40/// and a 64 MiB response buffer.41export fn create(sender: Handler.Send) -> Client {42  Client {43    name: Defaults.DEFAULT_NAME,44    send: sender,45    baseAddress: None,46    defaultHeaders: [],47    timeout: Defaults.CLIENT_TIMEOUT,48    maxResponseContentBufferSize: Defaults.MAX_RESPONSE_BUFFER,49    defaultVersion: Http.Http11,50    defaultVersionPolicy: Version.OrLower,51    pending: Shared.shared(Cancel.token())52  }53}5455/// A client sending straight through a transport.56export fn over(transport: &Transport.Transport) -> Client { create(Transport.send(transport)) }5758/// A long-lived client over a pooled transport of its own, configured once and shared: every copy59/// of it sends through the same pool. Give the options a pooled connection lifetime so the client60/// still sees addresses change.61export fn pooled(options: Transport.Options) -> Client { over(&Transport.create(options)) }6263/// Every reason the client's settings cannot be followed.64export fn validate(client: &Client) -> Array[Str] {65  var problems: Array[Str] = []66  if let Some(base) = client.baseAddress {67    if Option.isNone(&Uri.endpointOf(base)) { problems = problems.push("baseAddress must be an absolute http or https address: " + base) }68  }69  if client.timeout < 1 && client.timeout != Defaults.INFINITE { problems = problems.push("timeout must be positive or INFINITE") }70  if client.maxResponseContentBufferSize < 0 { problems = problems.push("maxResponseContentBufferSize must be at least 0") }71  if let Some(name) = Headers.unwritable(&client.defaultHeaders) { problems = problems.push(Template.fill(Messages.BAD_HEADER, &[name])) }72  problems73}7475/** @HttpClient.Client.Configuring — a client changed one decision at a time */76export trait Configuring {77  /// The client resolving relative addresses against a base address.78  fn withBaseAddress(self: &Self, address: Str) -> Self79  /// The client adding a header to every request that does not carry it.80  fn withDefaultHeader(self: &Self, name: Str, value: Str) -> Self81  /// The client giving each request this many milliseconds, or `Defaults.INFINITE`.82  fn withTimeout(self: &Self, millis: Int) -> Self83  /// The client buffering at most this many response body bytes.84  fn withMaxResponseContentBufferSize(self: &Self, bytes: Int) -> Self85  /// The client sending the requests its helpers build with this version and policy.86  fn withDefaultVersion(self: &Self, version: Http.Version, policy: Version.Policy) -> Self87  /// The client known by a name.88  fn named(self: &Self, name: Str) -> Self89}9091impl Configuring for Client {92  /// The client resolving relative addresses against a base address.93  fn withBaseAddress(self: &Self, address: Str) -> Self { Client{..*self, baseAddress: Some(address)} }9495  /// The client adding a header to every request that does not carry it.96  fn withDefaultHeader(self: &Self, name: Str, value: Str) -> Self { Client{..*self, defaultHeaders: Headers.add(&self.defaultHeaders, name, value)} }9798  /// The client giving each request this many milliseconds, or `Defaults.INFINITE`.99  fn withTimeout(self: &Self, millis: Int) -> Self { Client{..*self, timeout: millis} }100101  /// The client buffering at most this many response body bytes.102  fn withMaxResponseContentBufferSize(self: &Self, bytes: Int) -> Self { Client{..*self, maxResponseContentBufferSize: bytes} }103104  /// The client sending the requests its helpers build with this version and policy.105  fn withDefaultVersion(self: &Self, version: Http.Version, policy: Version.Policy) -> Self { Client{..*self, defaultVersion: version, defaultVersionPolicy: policy} }106107  /// The client known by a name.108  fn named(self: &Self, name: Str) -> Self { Client{..*self, name: name} }109}110111/// The response to a request, which may be cancelled only by the client's timeout and `cancelPending`.112export fn send(client: &Client, request: Request.Request) -> HttpClient.Outcome[Response.Response] {113  sendWith(client, request, Cancel.token())114}115116/// The response to a request: its address resolved against the base address, the default headers117/// it lacks added, and its body buffered within the client's limit; cancelled when `token` fires,118/// the client's timeout passes, or `cancelPending` is called.119export fn sendWith(client: &Client, request: Request.Request, token: Cancel.Token) -> HttpClient.Outcome[Response.Response] {120  let uri = resolved(client, request.uri) ?121  let prepared = Request.Request {122    ..request,123    uri: uri,124    headers: Headers.merged(&client.defaultHeaders, &request.headers),125    options: RequestOptions.set(&request.options, &Transport.bufferLimit(), client.maxResponseContentBufferSize)126  }127  let linked = Tokens.linked(&[token, Shared.current(&client.pending)], client.timeout)128  match (client.send)(prepared, linked) {129    case Err(HttpClient.TimedOut(elapsed)) => {130      if client.timeout != Defaults.INFINITE && !Cancel.stopped(&token) { Err(HttpClient.TimedOut(client.timeout)) } else { Err(HttpClient.TimedOut(elapsed)) }131    }132    case other => other133  }134}135136/// Cancels every request in flight and lets later requests run.137export fn cancelPending(client: &Client) -> () {138  let previous = Shared.change(&client.pending, |current: Cancel.Token| (Cancel.token(), current))139  let _cancelled = Cancel.cancel(&previous, PENDING_CANCELLED)140}141142/// A request the client's helpers build: the method and address, with the client's default version.143export fn build(client: &Client, method: Http.Method, uri: Str) -> Request.Request {144  Request.Request{..Request.create(method, uri), version: client.defaultVersion, versionPolicy: client.defaultVersionPolicy}145}146147/// The response to a `GET`, whatever its status.148export fn get(client: &Client, uri: Str) -> HttpClient.Outcome[Response.Response] { send(client, build(client, Http.Get, uri)) }149150/// The body of a successful `GET` as UTF-8 text.151export fn getText(client: &Client, uri: Str) -> HttpClient.Outcome[Str] {152  let response = Response.ensureSuccess(get(client, uri) ?) ?153  Response.text(&response)154}155156/// The body of a successful `GET` as bytes.157export fn getBytes(client: &Client, uri: Str) -> HttpClient.Outcome[Bytes] {158  let response = Response.ensureSuccess(get(client, uri) ?) ?159  Ok(Response.bytes(&response))160}161162/// The head of a successful `GET` whose body was handed to `receiver` as it arrived instead of being163/// buffered; `receiver` answers `false` to stop reading.164export fn stream(client: &Client, uri: Str, receiver: Request.Receiver) -> HttpClient.Outcome[Response.Response] {165  Response.ensureSuccess(send(client, Request.Request{..build(client, Http.Get, uri), receiver: Some(receiver)}) ?)166}167168/// The response to a `HEAD`, whatever its status.169export fn head(client: &Client, uri: Str) -> HttpClient.Outcome[Response.Response] { send(client, build(client, Http.Head, uri)) }170171/// The response to a `POST` carrying a body, whatever its status.172export fn post(client: &Client, uri: Str, content: Content.Content) -> HttpClient.Outcome[Response.Response] {173  send(client, Request.Request{..build(client, Http.Post, uri), content: Some(content)})174}175176/// The response to a `PUT` carrying a body, whatever its status.177export fn put(client: &Client, uri: Str, content: Content.Content) -> HttpClient.Outcome[Response.Response] {178  send(client, Request.Request{..build(client, Http.Put, uri), content: Some(content)})179}180181/// The response to a `PATCH` carrying a body, whatever its status.182export fn patch(client: &Client, uri: Str, content: Content.Content) -> HttpClient.Outcome[Response.Response] {183  send(client, Request.Request{..build(client, Http.Patch, uri), content: Some(content)})184}185186/// The response to a `DELETE`, whatever its status.187export fn delete(client: &Client, uri: Str) -> HttpClient.Outcome[Response.Response] { send(client, build(client, Http.Delete, uri)) }188189/// The address a request goes to: absolute as given, or resolved against the base address.190fn resolved(client: &Client, uri: Str) -> HttpClient.Outcome[Str] {191  let escaped = Uri.escape(uri)192  if Uri.isAbsolute(escaped) { return Ok(escaped) }193  match client.baseAddress {194    case Some(base) => Ok(Uri.resolve(base, escaped))195    case None => HttpClient.invalid(Template.fill(Messages.RELATIVE_WITHOUT_BASE, &[uri]))196  }197}198