Repository F# setup
open System
open System.Net.Http
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.HttpClient
open Axial.PlatformServiceTesting And Layers
This page shows how the single IHttp.Send boundary makes HTTP workflows testable without a mocking library.
A Complete Fake In A Few Lines
The service has one method, and Response.create builds synthetic transcripts from an explicit timestamp:
Shared setup
// Setup for the checked examples on this page.
open System
open System.Net.Http
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.HttpClient
open Axial.PlatformService
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual
SystemNetHttpThreadingTasksAxialHttpClientPlatformServiceshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
type User = { Id: int; Name: string }
/// A hand-written decoder for {"id":1,"name":"Ada"}; a JSON library's decoder fits the same signature.
let decodeUser (json: string) : Result<User, string> =
let found = Text.RegularExpressions.Regex.Match(json, """^\{"id":(\d+),"name":"([^"]*)"\}$""")
if found.Success then Ok { Id = int found.Groups[1].Value; Name = found.Groups[2].Value }
else Error $"not a user: {json}"
type TestEnv =
{ Http: IHttp }
interface IHasHttp with
member this.Http = this.Http
let stub status body =
let startedAt = DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)
{ Http =
{ new IHttp with
member _.Send(_, _) = async { return Ok(Response.create startedAt status body) } } }
FsLiveDocsGeneratedPage3_03F7AABF2A94.UserId: intintAn abbreviation for the CLI type . Basic Types
Name: stringstringAn abbreviation for the CLI type . Basic Types
decodeUser: string -> Result<User,string>json: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
found: Text.RegularExpressions.MatchTextMatch: string * string -> Text.RegularExpressions.MatchSearches the specified input string for the first occurrence of the specified regular expression. The string to search for a match. The regular expression pattern to match. An object that contains information about the match. A regular expression parsing error occurred. or is . A time-out occurred. For more information about time-outs, see the Remarks section.
RegularExpressionsSystem.Text.RegularExpressions.RegexRepresents an immutable regular expression.
Success: boolGets a value indicating whether the match is successful. if the match is successful; otherwise, .
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
int: ^T -> intConverts the argument to signed 32-bit integer. This is a direct conversion for all primitive numeric types. For strings, the input is converted using Int32.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted int int 'A' // evaluates to 65 int 0xff // evaluates to 255 int -10 // evaluates to -10
Groups: Text.RegularExpressions.GroupCollectionGets a collection of groups matched by the regular expression. The character groups matched by the pattern.
Item: Text.RegularExpressions.GroupEnables access to a member of the collection by integer index. The zero-based index of the collection member to be retrieved. The member of the collection specified by .
Value: stringGets the captured substring from the input string. The substring that is captured by the match.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
FsLiveDocsGeneratedPage3_03F7AABF2A94.TestEnvHttp: IHttpAxial.HttpClient.IHttpSends fully described HTTP requests for a concrete host platform.
Axial.HttpClient.IHasHttpDeclares that an environment supplies the HTTP service. Implement this on the environment supplied at the host edge. A workflow that sends requests constrains its environment with 'env :> IHasHttp.
this: TestEnvHttp: TestEnv -> unit -> IHttpstub: int -> string -> TestEnvstatus: intbody: stringstartedAt: DateTimeOffset``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
_: IHttpSend: HttpRequest * CancellationToken -> Async<Result<HttpResponse,HttpError>>async: AsyncBuilderBuilds an asynchronous workflow using computation expression syntax. let sleepExample() = async { printfn "sleeping" do! Async.Sleep 10 printfn "waking up" return 6 } sleepExample() |> Async.RunSynchronously
Axial.HttpClient.ResponseModulecreate: DateTimeOffset -> int -> string -> HttpResponseCreates a synthetic response transcript at an explicit start time, primarily for test fakes. <example><code>Response.create startedAt 200 """{"ok":true}"""</code></example>
A test runs the workflow against the stub and checks the result, here with shouldEqual; an xUnit test would use its
own assertion:
Http.getJson decodeUser "https://api.example.test/users/1"
|> Flow.run (stub 200 """{"id":1,"name":"Ada"}""")
|> shouldEqual (Exit.Success { Id = 1; Name = "Ada" })
Axial.HttpClient.HttpModulegetJson: (string -> Result<'value,string>) -> string -> Flow<'env,HttpError,'value>Sends a GET request and decodes the JSON response. <example><code>Http.getJson (Json.deserializeResult codec) "https://api.example.com/users/1"</code></example>
decodeUser: string -> Result<User,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
stub: int -> string -> TestEnvshouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Id: intName: stringBecause the fake receives the full HttpRequest, tests can also assert on what was sent: method, URL, query,
headers, and body are all plain data. Returning Error(HttpError.TimedOut(...)) from a fake exercises retry and
fallback paths deterministically, with no network and no clock.
The Live Service
Http.live adapts an explicit IClock and one HttpClient; Layer.succeed (Http.live …) exposes them as a layer:
open Axial.Layers
type AppEnv =
{ Http: IHttp }
interface IHasHttp with
member this.Http = this.Http
let appLayer (clock: IClock) (client: HttpClient) : Layer<unit, HttpError, AppEnv> =
layer {
let! http = Layer.succeed (Http.live clock client)
return { Http = http }
}
let runUserLookup (client: HttpClient) =
Http.getJson decodeUser "users/1"
|> Layer.provide (appLayer Clock.live client)
|> Flow.run ()
AxialLayersFsLiveDocsGeneratedPage3_03F7AABF2A94.AppEnvHttp: IHttpAxial.HttpClient.IHttpSends fully described HTTP requests for a concrete host platform.
Axial.HttpClient.IHasHttpDeclares that an environment supplies the HTTP service. Implement this on the environment supplied at the host edge. A workflow that sends requests constrains its environment with 'env :> IHasHttp.
this: AppEnvHttp: AppEnv -> unit -> IHttpappLayer: IClock -> HttpClient -> Layer<unit,HttpError,AppEnv>clock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
client: HttpClientSystem.Net.Http.HttpClientProvides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.
Axial.Layers.Layer`3Represents a provisioning step that builds an explicit environment inside a scope. The input environment required to build the layer. The typed failure produced during provisioning. The environment or service bundle produced by the layer.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
layer: LayerBuilderThe layer { } computation expression for provisioning explicit service environments.
http: IHttpAxial.Layers.LayerModulesucceed: 'output -> Layer<'input,'error,'output>Creates a layer that succeeds with a fixed output value.
Axial.HttpClient.HttpModulelive: IClock -> HttpClient -> IHttpCreates a live HTTP service backed by an explicit clock and <see cref="T:System.Net.Http.HttpClient" />. <example><code>Http.live Clock.live (new HttpClient())</code></example>
runUserLookup: HttpClient -> Exit<User,HttpError>getJson: (string -> Result<'value,string>) -> string -> Flow<'env,HttpError,'value>Sends a GET request and decodes the JSON response. <example><code>Http.getJson (Json.deserializeResult codec) "https://api.example.com/users/1"</code></example>
decodeUser: string -> Result<User,string>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
provide: Layer<'input,'error,'environment> -> Flow<'environment,'error,'value> -> Flow<'input,'error,'value>Builds an environment with a layer, runs a downstream flow, and always closes the layer scope. This is the provisioning boundary. It creates a fresh scope, builds the supplied layer inside that scope, runs the downstream flow with the built environment, and finalizes all acquired resources when the downstream flow completes or fails. The layer that builds the downstream environment. The flow to run with the provided environment. A flow that requires only the input environment of the layer. let program () = let runtimeLayer = Layer.succeed "production" let workflow = Flow.envWith String.length Layer.provide runtimeLayer workflow
Axial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
Reuse one HttpClient per application, exactly as .NET recommends: connection pooling, DNS rotation handlers,
and proxy settings stay standard HttpClient concerns. Axial adds the typed request/response boundary on top
without hiding the client or the clock used for transcript timestamps and durations. Tests can pass Clock.fromValue
or another IClock fake for deterministic time.
Base addresses configured on the client work as usual: relative request URLs resolve against
client.BaseAddress:
let apiClient () = new HttpClient(BaseAddress = Uri "https://api.example.com/")
// Http.getJson decodeUser "users/1" now resolves to https://api.example.com/users/1
apiClient: unit -> HttpClientSystem.Net.Http.HttpClientProvides a base class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.
BaseAddress: UriGets or sets the base address of Uniform Resource Identifier (URI) of the Internet resource used when sending requests. The base address of Uniform Resource Identifier (URI) of the Internet resource used when sending requests.
``.ctor``: string -> unitInitializes a new instance of the class with the specified URI. A string that identifies the resource to be represented by the instance. Note that an IPv6 address in string form must be enclosed within brackets. For example, "http://[2607:f8b0:400d:c06::69]". is . In the .NET for Windows Store apps or the Portable Class Library, catch the base class exception, , instead. is empty. -or- The scheme specified in is not correctly formed. See . -or- contains too many slashes. -or- The password specified in is not valid. -or- The host name specified in is not valid. -or- The file name specified in is not valid. -or- The user name specified in is not valid. -or- The host or authority name specified in cannot be terminated by backslashes. -or- The port number specified in is not valid or cannot be parsed. -or- The length of exceeds 65519 characters. -or- The length of the scheme specified in exceeds 1023 characters. -or- There is an invalid character sequence in . -or- The MS-DOS path specified in must start with c:\\.
Composing With Other Services
Service records compose the same way as the other platform packages:
type WorkerEnv =
{ Http: IHttp
Clock: IClock }
interface IHasHttp with member this.Http = this.Http
interface IHasClock with member this.Clock = this.Clock
FsLiveDocsGeneratedPage3_03F7AABF2A94.WorkerEnvHttp: IHttpAxial.HttpClient.IHttpSends fully described HTTP requests for a concrete host platform.
Clock: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
Axial.HttpClient.IHasHttpDeclares that an environment supplies the HTTP service. Implement this on the environment supplied at the host edge. A workflow that sends requests constrains its environment with 'env :> IHasHttp.
this: WorkerEnvHttp: WorkerEnv -> unit -> IHttpAxial.IHasClockDeclares the clock required by timed workflows and fiber diagnostics.
Clock: WorkerEnv -> unit -> IClockThe same pattern adds IHasProcess for Axial.Process, or any other package's contract.
A workflow that needs both declares Flow<WorkerEnv, ...> (or stays polymorphic with
'env :> IHasHttp constraints) and runs against one environment value.
Portability
Request construction, the Request/Response modules, HttpError, and the DSL are portable and compile under
Fable. The Http.live service and Layer.succeed (Http.live …) are .NET-only: on other hosts, implement IHttp over the
platform's fetch primitive and provide it through the same environment record.
When Not To Fake
Fakes verify workflow logic, not server behavior. A small number of tests against a real endpoint (a loopback listener works well) cover the live service's encoding, header, timeout, and error mapping; the package's own test suite does this.

