Skip to content

Instantly share code, notes, and snippets.

@jhewlett
Last active May 14, 2023 15:42
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jhewlett/865c11442ab383a5ada5d0c6cad174d1 to your computer and use it in GitHub Desktop.
Functional wrapper around System.Net.Http.HttpClient. Inspired in part by Http.fs (https://github.com/haf/Http.fs) and FSharp.Data (https://fsharp.github.io/FSharp.Data/library/Http.html)
namespace HttpClient.FSharp
open System
open System.Net.Http
type HttpMethod =
| Post
| Put
| Delete
| Get
module HttpMethod =
let value method =
match method with
| Post -> System.Net.Http.HttpMethod.Post
| Put -> System.Net.Http.HttpMethod.Put
| Delete -> System.Net.Http.HttpMethod.Delete
| Get -> System.Net.Http.HttpMethod.Get
[<RequireQualifiedAccess>]
type RequestBody =
| Json of string
module RequestBody =
let value body =
match body with
| RequestBody.Json json -> new StringContent(json, System.Text.Encoding.UTF8, "application/json")
type Request = {
Url : string
Method : HttpMethod
Timeout : TimeSpan option
Headers : (string * string) list
Body : RequestBody option
Query : (string * string) list
}
[<AutoOpen>]
module Request =
let withTimeout timeout request =
{ request with Timeout = Some timeout }
let withBody body request =
{ request with Body = Some body }
let withHeader header request =
{ request with Headers = header :: request.Headers }
let withQueryParam param request =
{ request with Query = param :: request.Query }
type Response = {
StatusCode : int
Body : string
Headers : (string * string) list
}
module Http =
let createRequest url method =
{
Url = url
Method = method
Timeout = None
Headers = []
Body = None
Query = []
}
module Url =
let private encodeUrlParam param =
System.Uri.EscapeDataString param
let appendQueryToUrl (url : string) query =
match query with
| [] -> url
| query ->
url
+ if url.Contains "?" then "&" else "?"
+ String.concat "&" [ for k, v in List.rev query -> encodeUrlParam k + "=" + encodeUrlParam v ]
let execute (httpClientFactory : IHttpClientFactory) (request : Request) : Async<Response> =
async {
use httpClient = httpClientFactory.CreateClient()
request.Timeout
|> Option.iter (fun t -> httpClient.Timeout <- t)
let fullUrl = Url.appendQueryToUrl request.Url request.Query
use requestMessage = new HttpRequestMessage(request.Method |> HttpMethod.value, fullUrl)
request.Headers
|> List.iter requestMessage.Headers.Add
request.Body
|> Option.iter (fun b ->
let body = RequestBody.value b
requestMessage.Content <- body)
use! response = httpClient.SendAsync(requestMessage) |> Async.AwaitTask
let! body = response.Content.ReadAsStringAsync() |> Async.AwaitTask
return
{
StatusCode = int response.StatusCode
Body = body
Headers = [for (KeyValue (k, v)) in response.Headers -> (k, String.concat "," v)]
}
}
module Main
open System
open Microsoft.Extensions.DependencyInjection
open System.Net.Http
open HttpClient.FSharp
let createHttpClientFactory () =
let services = ServiceCollection()
services.AddHttpClient() |> ignore
let serviceProvider = services.BuildServiceProvider()
serviceProvider.GetRequiredService<IHttpClientFactory>()
[<EntryPoint>]
let main argv =
let httpClientFactory = createHttpClientFactory ()
let request =
Http.createRequest "https://hacker-news.firebaseio.com/v0/item/8863.json" Get
|> withTimeout (TimeSpan.FromSeconds 2.)
|> withHeader ("Accept", "application/json")
|> withQueryParam ("print", "pretty")
let response =
request
|> Http.execute httpClientFactory
|> Async.RunSynchronously
printfn "Status: %d; Body: %s" response.StatusCode response.Body
0
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Http.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
</ItemGroup>
</Project>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment