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
A Complete Fake In A Few Lines
The service has one method, and Response.create builds synthetic transcripts from an explicit timestamp:
Show page setup
// Setup for the checked examples on this page.
open System
open System.Net.Http
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 actualtype 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(_) = Flow.succeed (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 -> Flow<unit,HttpError,HttpResponse>Axial.Flowsucceed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
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>
Requires & types
Requires: open Axial; open Axial.HttpClient; open System (packages: Axial, Axial.HttpClient)val decodeUser: string -> Result<User,string>val stub: int -> string -> TestEnvA 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: stringRequires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)Because the fake receives the full HttpRequest, tests can also assert on what was sent: method, URL, query,
headers, and body are all plain data. IHttp.Send returns a Flow, so a fake reports a failure with Flow.fail,
not a bare Result:
/// A fake that always fails with a per-request timeout; no network and no clock are involved.
let alwaysTimesOut =
{ Http =
{ new IHttp with
member _.Send(request) =
Flow.fail (HttpError.TimedOut(Request.render request, TimeSpan.FromSeconds 5.0)) } }
alwaysTimesOut: TestEnvHttp: IHttpAxial.HttpClient.IHttpSends fully described HTTP requests for a concrete host platform.
_: IHttpSend: HttpRequest -> Flow<unit,HttpError,HttpResponse>request: HttpRequestAxial.Flowfail: 'error -> Flow<'env,'error,'value>Same as error. The error value to wrap in a failing flow. A flow that always fails with the provided error. let result = Flow.fail "error" |> Flow.run () // result = Failure (Cause.Fail "error")
Axial.HttpClient.HttpErrorA recoverable HTTP transport, timeout, status, or decoding failure.
TimedOutThe per-request timeout elapsed before the response completed.
Axial.HttpClient.RequestModulerender: HttpRequest -> stringRenders the redacted request line used in error transcripts. <example><code>Request.render request</code></example>
System.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
Requires & types
Requires: open Axial; open Axial.HttpClient; open System (packages: Axial, Axial.HttpClient)val alwaysTimesOut: TestEnvReturning a typed failure such as HttpError.TimedOut from a fake exercises retry and fallback paths
deterministically, with no network and no clock.
A scripted double for sequences
When a test needs several requests to answer in turn — two transient failures, then success — a scripted double dequeues one reply per request and records what was sent. The reliability guide uses this shape for its retry examples:
/// An HTTP service that replies with each status and body in turn, and records the requests it received.
type ScriptedHttp(replies: (int * string) list) =
let remaining = Collections.Generic.Queue(replies)
let sent = ResizeArray<HttpRequest>()
member _.Sent = List.ofSeq sent
member this.Env = { Http = this }
interface IHttp with
member _.Send(request) =
Flow.delay (fun () ->
sent.Add request
let status, body = remaining.Dequeue()
Flow.succeed (Response.create (DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)) status body))
FsLiveDocsGeneratedPage3_03F7AABF2A94.ScriptedHttpreplies: (int * string) listintAn abbreviation for the CLI type . Basic Types
stringAn abbreviation for the CLI type . Basic Types
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
remaining: Collections.Generic.Queue<int * string>Collections``.ctor``: (int * string) seq -> unitInitializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. The collection whose elements are copied to the new . is .
Genericsent: ResizeArray<HttpRequest>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
Axial.HttpClient.HttpRequestAn immutable, fully described HTTP request.
_: ScriptedHttpSent: ScriptedHttp -> unit -> HttpRequest listMicrosoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
this: ScriptedHttpEnv: ScriptedHttp -> unit -> TestEnvHttp: IHttpAxial.HttpClient.IHttpSends fully described HTTP requests for a concrete host platform.
Send: ScriptedHttp -> HttpRequest -> Flow<unit,HttpError,HttpResponse>request: HttpRequestAxial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Add: HttpRequest -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
status: intbody: stringDequeue: unit -> int * stringRemoves and returns the object at the beginning of the . The object that is removed from the beginning of the . The is empty.
succeed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
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>
``.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.
Requires & types
Requires: open Axial; open Axial.HttpClient; open System (packages: Axial, Axial.HttpClient)Build one with the replies it should return in order, run the workflow against its Env, then assert on Sent to
check how many attempts were made and what each request carried.
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
Requires & types
Requires: open Axial; open Axial.HttpClient; open Axial.Layers; open Axial.PlatformService; open System.Net.Http (packages: Axial, Axial.HttpClient, Axial.Layers, Axial.PlatformService)val appLayer: IClock -> HttpClient -> Layer<unit,HttpError,AppEnv>val runUserLookup: HttpClient -> Exit<User,HttpError>runUserLookup sends the relative URL "users/1", so the caller must supply a client whose BaseAddress is
set, or the lookup must use an absolute URL. Without a base address, Http.live fails the request with
HttpError.InvalidRequest.
Reuse one HttpClient per application: connection pooling, DNS rotation handlers,
and proxy settings remain 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:\\.
Requires & types
Requires: open System.Net.Httpval apiClient: unit -> HttpClientComposing 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 -> IClockRequires & types
Requires: open Axial; open Axial.HttpClient (packages: Axial, Axial.HttpClient)The 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.

