Current section
34 Versions
Jump to
Current section
34 Versions
Compare versions
6
files changed
+540
additions
-30
deletions
| @@ -1,18 +1,30 @@ | |
| 1 | - # gleam_http |
| 1 | + # Gleam HTTP |
| 2 2 | |
| 3 | - A Gleam library that provides HTTP types to be used by HTTP servers and |
| 4 | - clients written in Gleam. |
| 3 | + Types and functions for HTTP clients and servers! |
| 5 4 | |
| 5 | + ## HTTP Service Example |
| 6 6 | |
| 7 | - ## Quick start |
| 7 | + ```rust |
| 8 | + import gleam/elli |
| 9 | + import gleam/http.{Request, Response} |
| 10 | + import gleam/bit_builder.{BitBuilder} |
| 11 | + import gleam/bit_string |
| 8 12 | |
| 9 | - ```sh |
| 10 | - # Build the project |
| 11 | - rebar3 compile |
| 13 | + // Define a HTTP service |
| 14 | + // |
| 15 | + pub fn my_service(req: Request(BitString)) -> Response(BitBuilder) { |
| 16 | + let body = "Hello, world!" |
| 17 | + |> bit_string.from_string |
| 18 | + |> bit_builder.from_bit_string |
| 12 19 | |
| 13 | - # Run the eunit tests |
| 14 | - rebar3 eunit |
| 20 | + http.response(200) |
| 21 | + |> http.prepend_resp_header("made-with", "Gleam") |
| 22 | + |> http.set_resp_body(body) |
| 23 | + } |
| 15 24 | |
| 16 | - # Run the Erlang REPL |
| 17 | - rebar3 shell |
| 25 | + // Start it on port 3000 using the Elli web server |
| 26 | + // |
| 27 | + pub fn start() { |
| 28 | + elli.start(my_service, on_port: 3000) |
| 29 | + } |
| 18 30 | ``` |
| @@ -1,10 +1,10 @@ | |
| 1 1 | {<<"app">>,<<"gleam_http">>}. |
| 2 2 | {<<"build_tools">>,[<<"rebar3">>]}. |
| 3 | - {<<"description">>,<<"A Gleam library">>}. |
| 3 | + {<<"description">>,<<"Types and functions for HTTP clients and servers!">>}. |
| 4 4 | {<<"files">>, |
| 5 5 | [<<"LICENSE">>,<<"README.md">>,<<"rebar.config">>,<<"rebar.lock">>, |
| 6 | - <<"src/gleam/http.gleam">>,<<"src/gleam_http.app.src">>, |
| 7 | - <<"src/gleam_http_native.erl">>]}. |
| 6 | + <<"src/gleam/http.gleam">>,<<"src/gleam/http/middleware.gleam">>, |
| 7 | + <<"src/gleam_http.app.src">>,<<"src/gleam_http_native.erl">>]}. |
| 8 8 | {<<"licenses">>,[<<"Apache 2.0">>]}. |
| 9 9 | {<<"links">>,[]}. |
| 10 10 | {<<"name">>,<<"gleam_http">>}. |
| @@ -13,4 +13,4 @@ | |
| 13 13 | [{<<"app">>,<<"gleam_stdlib">>}, |
| 14 14 | {<<"optional">>,false}, |
| 15 15 | {<<"requirement">>,<<"0.10.0">>}]}]}. |
| 16 | - {<<"version">>,<<"1.0.0">>}. |
| 16 | + {<<"version">>,<<"1.1.0">>}. |
| @@ -2,16 +2,17 @@ | |
| 2 2 | {src_dirs, ["src", "gen/src"]}. |
| 3 3 | |
| 4 4 | {profiles, [ |
| 5 | - {test, [ |
| 6 | - {pre_hooks, [{compile, "gleam build ."}]}, |
| 7 | - {src_dirs, ["src", "test", "gen/src", "gen/test"]} |
| 8 | - ]}, |
| 9 | - |
| 10 | - {dev, [ |
| 11 | - {pre_hooks, [{compile, "gleam build ."}]} |
| 12 | - ]} |
| 5 | + {test, [{src_dirs, ["src", "test", "gen/src", "gen/test"]}]} |
| 13 6 | ]}. |
| 14 7 | |
| 8 | + {shell, [ |
| 9 | + % {config, "config/sys.config"}, |
| 10 | + {apps, [gleam_stdlib]} |
| 11 | + ]}. |
| 12 | + |
| 13 | + |
| 14 | + {project_plugins, [rebar_gleam, rebar3_hex]}. |
| 15 | + |
| 15 16 | {deps, [ |
| 16 17 | {gleam_stdlib, "0.10.0"} |
| 17 18 | ]}. |
| @@ -1,8 +1,24 @@ | |
| 1 | - import gleam/string |
| 1 | + //// Functions for working with HTTP data structures in Gleam. |
| 2 | + //// |
| 3 | + //// This module makes it easy to create and modify Requests and Responses, data types. |
| 4 | + //// A general HTTP message type is defined that enables functions to work on both requests and responses. |
| 5 | + //// |
| 6 | + //// This module does not implement a HTTP client or HTTP server, but it can be used as a base for them. |
| 2 7 | |
| 3 | - /// HTTP methods as defined by RFC 2616, and PATCH which is defined by RFC |
| 4 | - /// 5789. |
| 5 | - /// |
| 8 | + // TODO: validate_req |
| 9 | + // TODO: set_resp_header |
| 10 | + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L776 |
| 11 | + // TODO: set_req_header |
| 12 | + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L776 |
| 13 | + import gleam/list |
| 14 | + import gleam/option.{None, Option, Some} |
| 15 | + import gleam/string |
| 16 | + import gleam/string_builder |
| 17 | + import gleam/uri.{Uri} |
| 18 | + import gleam/dynamic.{Dynamic} |
| 19 | + |
| 20 | + /// HTTP standard method as defined by [RFC 2616](https://tools.ietf.org/html/rfc2616), |
| 21 | + /// and PATCH which is defined by [RFC 5789](https://tools.ietf.org/html/rfc5789). |
| 6 22 | pub type Method { |
| 7 23 | Get |
| 8 24 | Post |
| @@ -50,5 +66,410 @@ pub fn method_to_string(method) { | |
| 50 66 | } |
| 51 67 | } |
| 52 68 | |
| 53 | - pub external fn method_from_erlang(anything) -> Result(Method, Nil) = |
| 69 | + /// The two URI schemes for HTTP |
| 70 | + /// |
| 71 | + pub type Scheme { |
| 72 | + Http |
| 73 | + Https |
| 74 | + } |
| 75 | + |
| 76 | + /// Convert a scheme into a string. |
| 77 | + /// |
| 78 | + /// # Examples |
| 79 | + /// |
| 80 | + /// > scheme_to_string(Http) |
| 81 | + /// "http" |
| 82 | + /// |
| 83 | + /// > scheme_to_string(Https) |
| 84 | + /// "https" |
| 85 | + /// |
| 86 | + pub fn scheme_to_string(scheme: Scheme) -> String { |
| 87 | + case scheme { |
| 88 | + Http -> "http" |
| 89 | + Https -> "https" |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + /// Parse a HTTP scheme from a string |
| 94 | + /// |
| 95 | + /// # Examples |
| 96 | + /// |
| 97 | + /// > scheme_to_string("http") |
| 98 | + /// Ok(Http) |
| 99 | + /// |
| 100 | + /// > scheme_to_string("ftp") |
| 101 | + /// Error(Nil) |
| 102 | + /// |
| 103 | + pub fn scheme_from_string(scheme: String) -> Result(Scheme, Nil) { |
| 104 | + case string.lowercase(scheme) { |
| 105 | + "http" -> Ok(Http) |
| 106 | + "https" -> Ok(Https) |
| 107 | + _ -> Error(Nil) |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + pub external fn method_from_dynamic(Dynamic) -> Result(Method, Nil) = |
| 54 112 | "gleam_http_native" "method_from_erlang" |
| 113 | + |
| 114 | + /// A HTTP header is a key-value pair. Header keys should be all lowercase |
| 115 | + /// characters. |
| 116 | + pub type Header = |
| 117 | + tuple(String, String) |
| 118 | + |
| 119 | + // TODO: document |
| 120 | + pub type Request(body) { |
| 121 | + Request( |
| 122 | + method: Method, |
| 123 | + headers: List(Header), |
| 124 | + body: body, |
| 125 | + scheme: Scheme, |
| 126 | + host: String, |
| 127 | + port: Option(Int), |
| 128 | + path: String, |
| 129 | + query: Option(String), |
| 130 | + ) |
| 131 | + } |
| 132 | + |
| 133 | + // TODO: document |
| 134 | + pub type Response(body) { |
| 135 | + Response(status: Int, headers: List(Header), body: body) |
| 136 | + } |
| 137 | + |
| 138 | + // TODO: document |
| 139 | + pub type Service(in, out) = |
| 140 | + fn(Request(in)) -> Response(out) |
| 141 | + |
| 142 | + /// Return the uri that a request was sent to. |
| 143 | + /// |
| 144 | + pub fn req_to_uri(request: Request(a)) -> Uri { |
| 145 | + Uri( |
| 146 | + scheme: Some(scheme_to_string(request.scheme)), |
| 147 | + userinfo: None, |
| 148 | + host: Some(request.host), |
| 149 | + port: request.port, |
| 150 | + path: request.path, |
| 151 | + query: request.query, |
| 152 | + fragment: None, |
| 153 | + ) |
| 154 | + } |
| 155 | + |
| 156 | + /// Construct a request from a URI. |
| 157 | + /// |
| 158 | + pub fn req_from_uri(uri: Uri) -> Result(Request(String), Nil) { |
| 159 | + try scheme = uri.scheme |
| 160 | + |> option.unwrap("") |
| 161 | + |> scheme_from_string |
| 162 | + try host = uri.host |
| 163 | + |> option.to_result(Nil) |
| 164 | + let req = Request( |
| 165 | + method: Get, |
| 166 | + headers: [], |
| 167 | + body: "", |
| 168 | + scheme: scheme, |
| 169 | + host: host, |
| 170 | + port: uri.port, |
| 171 | + path: uri.path, |
| 172 | + query: uri.query, |
| 173 | + ) |
| 174 | + Ok(req) |
| 175 | + } |
| 176 | + |
| 177 | + /// Construct an empty Response. |
| 178 | + /// |
| 179 | + /// The body type of the returned response is `Nil`, and should be set with a |
| 180 | + /// call to `set_resp_body`. |
| 181 | + /// |
| 182 | + pub fn response(status: Int) -> Response(Nil) { |
| 183 | + Response(status: status, headers: [], body: Nil) |
| 184 | + } |
| 185 | + |
| 186 | + /// Return the non-empty segments of a request path. |
| 187 | + /// |
| 188 | + pub fn path_segments(request: Request(body)) -> List(String) { |
| 189 | + request.path |
| 190 | + |> uri.path_segments |
| 191 | + } |
| 192 | + |
| 193 | + /// Decode the query of a request. |
| 194 | + pub fn get_query( |
| 195 | + request: Request(body), |
| 196 | + ) -> Result(List(tuple(String, String)), Nil) { |
| 197 | + case request.query { |
| 198 | + Some(query_string) -> uri.parse_query(query_string) |
| 199 | + None -> Ok([]) |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + // TODO: test |
| 204 | + // TODO: escape |
| 205 | + // TODO: record update syntax |
| 206 | + /// Set the query of the request. |
| 207 | + /// |
| 208 | + pub fn set_query( |
| 209 | + req: Request(body), |
| 210 | + query: List(tuple(String, String)), |
| 211 | + ) -> Request(body) { |
| 212 | + let pair = fn(t: tuple(String, String)) { |
| 213 | + string_builder.from_strings([t.0, "=", t.1]) |
| 214 | + } |
| 215 | + let query = query |
| 216 | + |> list.map(pair) |
| 217 | + |> list.intersperse(string_builder.from_string("&")) |
| 218 | + |> string_builder.concat |
| 219 | + |> string_builder.to_string |
| 220 | + |> Some |
| 221 | + let Request( |
| 222 | + method: method, |
| 223 | + headers: headers, |
| 224 | + body: body, |
| 225 | + scheme: scheme, |
| 226 | + host: host, |
| 227 | + port: port, |
| 228 | + path: path, |
| 229 | + query: _, |
| 230 | + ) = req |
| 231 | + Request( |
| 232 | + method: method, |
| 233 | + headers: headers, |
| 234 | + body: body, |
| 235 | + scheme: scheme, |
| 236 | + host: host, |
| 237 | + port: port, |
| 238 | + path: path, |
| 239 | + query: query, |
| 240 | + ) |
| 241 | + } |
| 242 | + |
| 243 | + /// Get the value for a given header. |
| 244 | + /// |
| 245 | + /// If the request does not have that header then `Error(Nil)` is returned. |
| 246 | + /// |
| 247 | + pub fn get_req_header( |
| 248 | + request: Request(body), |
| 249 | + key: String, |
| 250 | + ) -> Result(String, Nil) { |
| 251 | + list.key_find(request.headers, string.lowercase(key)) |
| 252 | + } |
| 253 | + |
| 254 | + /// Get the value for a given header. |
| 255 | + /// |
| 256 | + /// If the request does not have that header then `Error(Nil)` is returned. |
| 257 | + /// |
| 258 | + pub fn get_resp_header( |
| 259 | + response: Response(body), |
| 260 | + key: String, |
| 261 | + ) -> Result(String, Nil) { |
| 262 | + list.key_find(response.headers, string.lowercase(key)) |
| 263 | + } |
| 264 | + |
| 265 | + // TODO: use record update syntax |
| 266 | + // TODO: document |
| 267 | + // TODO: test |
| 268 | + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L809 |
| 269 | + pub fn prepend_req_header( |
| 270 | + request: Request(body), |
| 271 | + key: String, |
| 272 | + value: String, |
| 273 | + ) -> Request(body) { |
| 274 | + let Request(method, headers, body, scheme, host, port, path, query) = request |
| 275 | + let headers = [tuple(string.lowercase(key), value), ..headers] |
| 276 | + Request(method, headers, body, scheme, host, port, path, query) |
| 277 | + } |
| 278 | + |
| 279 | + // TODO: use record update syntax |
| 280 | + // TODO: document |
| 281 | + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L809 |
| 282 | + pub fn prepend_resp_header( |
| 283 | + response: Response(body), |
| 284 | + key: String, |
| 285 | + value: String, |
| 286 | + ) -> Response(body) { |
| 287 | + let Response(status, headers, body) = response |
| 288 | + let headers = [tuple(string.lowercase(key), value), ..headers] |
| 289 | + Response(status, headers, body) |
| 290 | + } |
| 291 | + |
| 292 | + /// Set the body of the response, overwriting any existing body. |
| 293 | + /// |
| 294 | + pub fn set_resp_body( |
| 295 | + response: Response(old_body), |
| 296 | + body: new_body, |
| 297 | + ) -> Response(new_body) { |
| 298 | + let Response(status: status, headers: headers, ..) = response |
| 299 | + Response(status: status, headers: headers, body: body) |
| 300 | + } |
| 301 | + |
| 302 | + // TODO: test |
| 303 | + // TODO: record update syntax |
| 304 | + /// Set the body of the request, overwriting any existing body. |
| 305 | + /// |
| 306 | + pub fn set_req_body( |
| 307 | + req: Request(old_body), |
| 308 | + body: new_body, |
| 309 | + ) -> Request(new_body) { |
| 310 | + let Request( |
| 311 | + method: method, |
| 312 | + headers: headers, |
| 313 | + scheme: scheme, |
| 314 | + host: host, |
| 315 | + port: port, |
| 316 | + path: path, |
| 317 | + query: query, |
| 318 | + .., |
| 319 | + ) = req |
| 320 | + Request( |
| 321 | + method: method, |
| 322 | + headers: headers, |
| 323 | + body: body, |
| 324 | + scheme: scheme, |
| 325 | + host: host, |
| 326 | + port: port, |
| 327 | + path: path, |
| 328 | + query: query, |
| 329 | + ) |
| 330 | + } |
| 331 | + |
| 332 | + // TODO: test |
| 333 | + // TODO: record update syntax |
| 334 | + /// Set the method of the request. |
| 335 | + /// |
| 336 | + pub fn set_method(req: Request(body), method: Method) -> Request(body) { |
| 337 | + let Request( |
| 338 | + method: _, |
| 339 | + headers: headers, |
| 340 | + body: body, |
| 341 | + scheme: scheme, |
| 342 | + host: host, |
| 343 | + port: port, |
| 344 | + path: path, |
| 345 | + query: query, |
| 346 | + ) = req |
| 347 | + Request( |
| 348 | + method: method, |
| 349 | + headers: headers, |
| 350 | + body: body, |
| 351 | + scheme: scheme, |
| 352 | + host: host, |
| 353 | + port: port, |
| 354 | + path: path, |
| 355 | + query: query, |
| 356 | + ) |
| 357 | + } |
| 358 | + |
| 359 | + // TODO: test |
| 360 | + /// Update the body of a response using a given function. |
| 361 | + /// |
| 362 | + pub fn map_resp_body( |
| 363 | + response: Response(old_body), |
| 364 | + transform: fn(old_body) -> new_body, |
| 365 | + ) -> Response(new_body) { |
| 366 | + response.body |
| 367 | + |> transform |
| 368 | + |> set_resp_body(response, _) |
| 369 | + } |
| 370 | + |
| 371 | + // TODO: test |
| 372 | + /// Update the body of a request using a given function. |
| 373 | + /// |
| 374 | + pub fn map_req_body( |
| 375 | + request: Request(old_body), |
| 376 | + transform: fn(old_body) -> new_body, |
| 377 | + ) -> Request(new_body) { |
| 378 | + request.body |
| 379 | + |> transform |
| 380 | + |> set_req_body(request, _) |
| 381 | + } |
| 382 | + |
| 383 | + // TODO: test |
| 384 | + /// Update the body of a response using a given result returning function. |
| 385 | + /// |
| 386 | + /// If the given function returns an `Ok` value the body is set, if it returns |
| 387 | + /// an `Error` value then the error is returned. |
| 388 | + /// |
| 389 | + pub fn try_map_resp_body( |
| 390 | + response: Response(old_body), |
| 391 | + transform: fn(old_body) -> Result(new_body, error), |
| 392 | + ) -> Result(Response(new_body), error) { |
| 393 | + try body = transform(response.body) |
| 394 | + Ok(set_resp_body(response, body)) |
| 395 | + } |
| 396 | + |
| 397 | + /// Create a response that redirects to the given uri. |
| 398 | + /// |
| 399 | + pub fn redirect(uri: String) -> Response(String) { |
| 400 | + Response( |
| 401 | + status: 303, |
| 402 | + headers: [tuple("location", uri)], |
| 403 | + body: string.append("You are being redirected to ", uri), |
| 404 | + ) |
| 405 | + } |
| 406 | + |
| 407 | + /// A request with commonly used default values. This request can be used as a |
| 408 | + /// an initial value and then update to create the desired request. |
| 409 | + /// |
| 410 | + pub fn default_req() -> Request(String) { |
| 411 | + Request( |
| 412 | + method: Get, |
| 413 | + headers: [], |
| 414 | + body: "", |
| 415 | + scheme: Https, |
| 416 | + host: "localhost", |
| 417 | + port: None, |
| 418 | + path: "", |
| 419 | + query: None, |
| 420 | + ) |
| 421 | + } |
| 422 | + |
| 423 | + // TODO: test |
| 424 | + // TODO: record update syntax |
| 425 | + /// Set the method of the request. |
| 426 | + /// |
| 427 | + pub fn set_host(req: Request(body), host: String) -> Request(body) { |
| 428 | + let Request( |
| 429 | + method: method, |
| 430 | + headers: headers, |
| 431 | + body: body, |
| 432 | + scheme: scheme, |
| 433 | + host: _, |
| 434 | + port: port, |
| 435 | + path: path, |
| 436 | + query: query, |
| 437 | + ) = req |
| 438 | + Request( |
| 439 | + method: method, |
| 440 | + headers: headers, |
| 441 | + body: body, |
| 442 | + scheme: scheme, |
| 443 | + host: host, |
| 444 | + port: port, |
| 445 | + path: path, |
| 446 | + query: query, |
| 447 | + ) |
| 448 | + } |
| 449 | + |
| 450 | + // TODO: test |
| 451 | + // TODO: record update syntax |
| 452 | + /// Set the path of the request. |
| 453 | + /// |
| 454 | + pub fn set_path(req: Request(body), path: String) -> Request(body) { |
| 455 | + let Request( |
| 456 | + method: method, |
| 457 | + headers: headers, |
| 458 | + body: body, |
| 459 | + scheme: scheme, |
| 460 | + host: host, |
| 461 | + port: port, |
| 462 | + path: _, |
| 463 | + query: query, |
| 464 | + ) = req |
| 465 | + Request( |
| 466 | + method: method, |
| 467 | + headers: headers, |
| 468 | + body: body, |
| 469 | + scheme: scheme, |
| 470 | + host: host, |
| 471 | + port: port, |
| 472 | + path: path, |
| 473 | + query: query, |
| 474 | + ) |
| 475 | + } |
| @@ -0,0 +1,76 @@ | |
| 1 | + import gleam/http.{Delete, Patch, Post, Put, Request, Response, Service} |
| 2 | + import gleam/list |
| 3 | + import gleam/result |
| 4 | + |
| 5 | + pub type Middleware(before_req, before_resp, after_req, after_resp) = |
| 6 | + fn(Service(before_req, before_resp)) -> Service(after_req, after_resp) |
| 7 | + |
| 8 | + /// A middleware that transform the response body returned by the service using |
| 9 | + /// a given function. |
| 10 | + /// |
| 11 | + pub fn map_resp_body( |
| 12 | + service: Service(req, a), |
| 13 | + with mapper: fn(a) -> b, |
| 14 | + ) -> Service(req, b) { |
| 15 | + fn(req) { |
| 16 | + req |
| 17 | + |> service |
| 18 | + |> http.map_resp_body(mapper) |
| 19 | + } |
| 20 | + } |
| 21 | + |
| 22 | + /// A middleware that prepends a header to the request. |
| 23 | + /// |
| 24 | + pub fn prepend_resp_header( |
| 25 | + service: Service(req, resp), |
| 26 | + key: String, |
| 27 | + value: String, |
| 28 | + ) -> Service(req, resp) { |
| 29 | + fn(req) { |
| 30 | + req |
| 31 | + |> service |
| 32 | + |> http.prepend_resp_header(key, value) |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + fn ensure_post(req: Request(a)) { |
| 37 | + case req.method { |
| 38 | + Post -> Ok(req) |
| 39 | + _ -> Error(Nil) |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + fn get_override_method(req) { |
| 44 | + try query_params = http.get_query(req) |
| 45 | + try method = list.key_find(query_params, "_method") |
| 46 | + try method = http.parse_method(method) |
| 47 | + case method { |
| 48 | + Put | Patch | Delete -> Ok(method) |
| 49 | + _ -> Error(Nil) |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + /// A middleware that overrides an incoming POST request with a method given in |
| 54 | + /// the request's `_method` query paramerter. This is useful as web browsers |
| 55 | + /// typically only support GET and POST requests, but our application may |
| 56 | + /// expect other HTTP methods that are more semantically correct. |
| 57 | + /// |
| 58 | + /// The methods PUT, PATCH, and DELETE are accepted for overriding, all others |
| 59 | + /// are ignored. |
| 60 | + /// |
| 61 | + /// The `_method` query paramerter can be specified in a HTML form like so: |
| 62 | + /// |
| 63 | + /// <form method="POST" action="/item/1?_method=DELETE"> |
| 64 | + /// <button type="submit">Delete item</button> |
| 65 | + /// </form> |
| 66 | + /// |
| 67 | + pub fn method_override(service: Service(req, resp)) -> Service(req, resp) { |
| 68 | + fn(req) { |
| 69 | + req |
| 70 | + |> ensure_post |
| 71 | + |> result.then(get_override_method) |
| 72 | + |> result.map(http.set_method(req, _)) |
| 73 | + |> result.unwrap(req) |
| 74 | + |> service |
| 75 | + } |
| 76 | + } |
Loading more files…