How do I send an HTTP request from EK9 and handle the response?

← Web Services · Ref: Q2241

Use HTTPClient. It holds connections, so a try owns it and closes it: 'try' then '-> client <- HTTPClient()' (or 'HTTPClient(config)'). Build an immutable HTTPClientRequest from an HTTPMethod - one of GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, written exactly - and a URL, never text: the URL comes from the environment through Networking().urlFromEnv, or from run-time text through Networking().urlOf, and is derived with its withers. 'withHeader(headerName, headerValue)' and 'withBody(content)' return a new request; a header the client sets itself (Host, Content-Length, Connection, Expect, Upgrade, Transfer-Encoding) or one holding a line break is refused and gives an UNSET request. 'client.send(request)' blocks - cheap on virtual threads, so there is no async form - and returns Result of (HTTPClientResponse, HTTPError). Any HTTP status, 404 and 500 included, is a response: read 'status()', 'header(headerName)' (any case; names are lower case in 'headers()'), and the body as Bytes, decoded only when you say how - 'bodyText("UTF-8")' or 'bodyJson()', a Result of (JSON, String). Only a transport failure, a URL the policy refuses or a protocol error is an HTTPError: 'category()' is an HTTPErrorCategory (DNS_RESOLUTION, CONNECT_TIMEOUT, READ_TIMEOUT, TLS_FAILURE, CONNECTION_REFUSED, TOO_MANY_REDIRECTS, PROTOCOL_ERROR, REFUSED_BY_POLICY) you compare with '==', and nothing is thrown. HTTPClientConfig sets the connect and request timeouts (10 and 30 seconds by default), an explicit proxy, and whether redirects are followed - they are NOT by default. The client speaks HTTP/1.1 and always verifies TLS. HTTPClient is a gated type: a package under capability enforcement declares 'org.ek9.lang::HTTPClient'.

Example

#!ek9
defines module qa.webservices.httpclient

  defines service

    Orders for :/orders open

      operator += for :/
        -> content as String :=: CONTENT
        <- response as HTTPResponse: (received: content) with trait of HTTPResponse
          override status() as pure
            <- rtn as Integer: 201
          override content()
            <- rtn as Bytes: Bytes().withUtf8(`{"received":${received}}`)
          override contentType() as pure
            <- rtn as String: "application/json"
          default operator ?

  defines function

    placeOrder()
      -> port as Integer
      stdout <- Stdout()
      base <- Networking().urlOf(`http://127.0.0.1:${port}/orders`)
      post <- HTTPClientRequest(HTTPMethod("POST"), base)
      json <- post.withHeader("Content-Type", "application/json")
      order <- json.withBody(Bytes().withUtf8(`{"item":"tea","quantity":2}`))

      try
        -> client <- HTTPClient(HTTPClientConfig().withRequestTimeout(PT5S))
        result <- client.send(order)
        if result.isOk()
          response <- result.ok()
          stdout.println(`status ${response.status()}`)
          stdout.println(`content-type ${response.header("content-type")}`)
          parsed <- response.bodyJson()
          if parsed.isOk()
            stdout.println(`json ${parsed.ok()}`)
        else if result.isError()
          stdout.println(`not sent: ${result.error()}`)

      //The client sets Host itself, so a request that tries to is unset.
      redirected <- order.withHeader("Host", "elsewhere.example")
      stdout.println(`request with a Host header set: ${redirected?}`)

  defines application

    OrdersApp
      register Orders()

  defines program

    SendAnHttpRequest() with application of OrdersApp
      properties <- NetworkProperties(0, true)
      try
        -> httpServer <- HTTP(properties)
        caller <- (properties) is HTTPReadyHandler as function
          placeOrder(properties.port)
          server.stop()
        httpServer.serveUntilComplete(caller)

Common mistakes

E08275 — A URL written into the source is an endpoint hard-coded into the program, so urlOf refuses text the compiler can know. Here the port is only known at run time; an endpoint you call belongs in the environment, read with Networking().urlFromEnv. See ek9 -h E08275 for details.

Incorrect:

      base <- Networking().urlOf("http://127.0.0.1:8080/orders")

Correct:

      base <- Networking().urlOf(`http://127.0.0.1:${port}/orders`)
Other ways to ask this
  • How do I call a REST API from EK9?
  • How do I POST JSON with HTTPClient in EK9?
  • Does EK9 have an HTTP client?
  • How does EK9 report a failed HTTP request?
  • Why does HTTPClient.send return a Result in EK9?

Coming from another language?

Java: java.net.http.HttpClient throws IOException, HttpTimeoutException and SSLHandshakeException, so a forgotten catch is a crash; its builder follows redirects and reads proxy settings from system properties. Python: requests raises ConnectionError/Timeout and decodes text with a guessed encoding. Go: http.Get returns (resp, err) but nothing forces the err check. In EK9 send returns a Result the compiler makes you check, the body is decoded only with an explicit encoding, redirects and proxies happen only when configured, and the URL is a URL from the environment or run-time data - never a literal.

Keywords: HTTPClient, HTTPClientConfig, HTTPClientRequest, HTTPClientResponse, HTTPError, HTTPErrorCategory, HTTPMethod, POST JSON, REST client, bodyJson, http request, send