1 # ezhttp/src/client: fetch a URL. URL and HTTP parsing joined to the wire effect. 2 # Answers a structured Response (status + headers + body), not a hub-style 3 # "0\n…" string contract. 4 import Base 5 import ./url.bend as Url 6 import ./http.bend as Http 7 import ./body.bend as Body 8 import ./wire.bend as Wire 9 import ./auth.bend as Auth 10 11 # structured client result: transport/parse error, or an HTTP response 12 type Response is Data: 13 Err{why: String} 14 Response{status: U32, headers: List<&2, Http.Header>, body: String} 15 16 # transport status line: first line is a code, rest is the payload 17 def wire.code(raw: String) -> Maybe<&2, U32>: 18 U32.read(Http.first(String.split(raw, '\n'))) 19 20 # text after the first newline 21 def wire.text.of(c: Http.Cut) -> String: 22 match c: 23 case Http.NoCut{}: 24 "" 25 case Http.Cut{_before, after}: 26 after 27 28 def wire.text(raw: String) -> String: 29 wire.text.of(Http.divide(raw, "\n")) 30 31 # whether the wire status is success (0) 32 def wire.ok.of(m: Maybe<&2, U32>) -> Bool: 33 match m: 34 case None{}: 35 False{} 36 case Some{n}: 37 U32.is_eq(n, 0) 38 39 def wire.ok(raw: String) -> Bool: 40 wire.ok.of(wire.code(raw)) 41 42 # a torn or successful HTTP parse as a client Response 43 def from.reply(r: Http.Reply) -> Response: 44 match r: 45 case Http.Torn{why}: 46 Err{why} 47 case Http.Reply{status, headers, body}: 48 Response{status, headers, body} 49 50 # a finished exchange 51 def from.raw(raw: String, ok: Bool) -> Response: 52 match ok: 53 case False{}: 54 Err{wire.text(raw)} 55 case True{}: 56 from.reply(Http.parse(wire.text(raw))) 57 58 # HTTP(S) exchange with a pre-built request 59 def http.talk(secure: Bool, host: String, port: U32, req: String) 60 -> IO(Response): 61 do IO: 62 +raw : String <- Wire.wire.talk(secure, host, port, req) 63 IO.pure(Response, from.raw(raw, wire.ok(raw))) 64 65 # a URL that came apart into an HTTP exchange 66 def http.loc(method: String, l: Url.Loc, headers: List<&2, Http.Header>, 67 body: String) -> IO(Response): 68 match l: 69 case Url.Bad{why}: 70 IO.pure(Response, Err{why}) 71 case Url.Loc{+scheme, +host, port, path}: 72 http.talk(String.eq(scheme, "https"), host, port, 73 Http.request(method, host, path, headers, body)) 74 75 # GET 76 def client.get(url: String, headers: List<&2, Http.Header>) -> IO(Response): 77 http.loc("GET", Url.parse(url), headers, "") 78 79 # HEAD (RFC 9110 §9.3.2) 80 def client.head(url: String, headers: List<&2, Http.Header>) -> IO(Response): 81 http.loc("HEAD", Url.parse(url), headers, "") 82 83 # POST 84 def client.post(url: String, headers: List<&2, Http.Header>, body: Body.Body) 85 -> IO(Response): 86 http.loc("POST", Url.parse(url), headers, Body.body.payload(body)) 87 88 # PUT 89 def client.put(url: String, headers: List<&2, Http.Header>, body: Body.Body) 90 -> IO(Response): 91 http.loc("PUT", Url.parse(url), headers, Body.body.payload(body)) 92 93 # DELETE 94 def client.delete(url: String, headers: List<&2, Http.Header>, body: Body.Body) 95 -> IO(Response): 96 http.loc("DELETE", Url.parse(url), headers, Body.body.payload(body)) 97 98 # OPTIONS 99 def client.options(url: String, headers: List<&2, Http.Header>) -> IO(Response): 100 http.loc("OPTIONS", Url.parse(url), headers, "") 101 102 # Authorization: Bearer … (auth.bend) 103 def client.bearer(token: String) -> Http.Header: 104 Auth.auth.bearer(token) 105 106 # Authorization: Basic … (auth.bend; RFC 7617, RFC 4648 Base64) 107 def client.basic(user: String, password: String) -> Http.Header: 108 Auth.auth.basic(user, password)