Behaviour
The framework's ToAction (Http.Request command) instance is the canonical way for an integration to call an HTTP API and dispatch a follow-up command on the response. It unconditionally emits a command via Integration.emitCommand on success, and its type signature requires (Json.ToJSON command, KnownSymbol (NameOf command)) on the carried command parameter.
That works when the outbound's response is a command. It breaks when the outbound is fire-and-forget (no domain-command follow-up) — the consumer has no command type to plug in, and any inert/no-op type like:
data NoCommand = NoCommand
…cannot satisfy Json.ToJSON (nothing to serialise; nothing meaningful would round-trip) and has no NameOf symbol (the type isn't a real command). The constraint is too strong for the inert case.
Why this matters
Fire-and-forget outbounds are common: thank-you emails, audit notifications, "best effort" pings to systems that don't drive the domain. The event that justified the outbound is already recorded by the time the outbound runs — the outbound just needs to make the network call and log success/failure. No downstream command should be emitted.
Today the consumer has two unappealing options:
- Invent a sham command with hand-written
ToJSON / NameOf instances and a no-op decide. Adds dead command surface, pollutes the command catalogue, and makes the read of Service.hs confusing ("what is this NoOpEmailSent command and where is it triggered?").
- Bypass
Http.Client for this integration, calling the underlying HTTP library directly. Drops out of the Integration.Http.Request plumbing entirely just to avoid the unsatisfiable constraint.
Repro shape
-- The natural shape (does not compile):
let httpReq = ThirdPartyApi.send … (\_resp -> NoCommand) (\_err -> NoCommand)
result <- Http.postRaw @Json.Value httpReq … -- requires Json.ToJSON NoCommand
-- + KnownSymbol (NameOf NoCommand)
Workaround consumers currently apply
Re-implement the post-then-log-then-no-emit flow against Http.Client's lower-level primitives, like:
dispatchHttpRequest ::
forall command. Request command ->
Task IntegrationError (Result Text ())
dispatchHttpRequest httpReq = do
let baseRequest = Http.request |> Http.withUrl httpReq.url |> Http.withTimeout httpReq.timeoutSeconds
-- … hand-roll headers, body marshalling, status classification …
result <- Http.postRaw @Json.Value withHeaders contentType content |> Task.asResult
Task.yield (mapHttpResult result)
This duplicates the work ToAction (Http.Request command) already does — just so the consumer can stop carrying an inert command phantom type around.
Suggested fix
Add a Http.Request variant or ToAction instance for the no-command case. Sketch:
Option A — sibling instance. instance ToAction (Http.Request ()) (or Http.Request Void) where the action runs the request, classifies the response, and yields Nothing (no command emitted). The consumer types its callbacks as \_resp -> ().
Option B — explicit fire-and-forget combinator. Http.fireAndForget :: Http.Request command -> Task IntegrationError (Result Text ()). Skips command-emission unconditionally; the command type-parameter goes unused at runtime but the constraint is dropped from this entry point.
Option C — relax the constraint. Drop (Json.ToJSON command, KnownSymbol (NameOf command)) from the request type and move it onto Integration.emitCommand instead, where it actually matters. The ToAction instance can branch on whether onSuccess/onError return () vs a real command (e.g. via a Maybe command field).
Any of these gives consumers a typed, in-framework way to express "this outbound has no follow-up command" without inventing dead types or escaping to raw HTTP.
Behaviour
The framework's
ToAction (Http.Request command)instance is the canonical way for an integration to call an HTTP API and dispatch a follow-up command on the response. It unconditionally emits a command viaIntegration.emitCommandon success, and its type signature requires(Json.ToJSON command, KnownSymbol (NameOf command))on the carriedcommandparameter.That works when the outbound's response is a command. It breaks when the outbound is fire-and-forget (no domain-command follow-up) — the consumer has no command type to plug in, and any inert/no-op type like:
…cannot satisfy
Json.ToJSON(nothing to serialise; nothing meaningful would round-trip) and has noNameOfsymbol (the type isn't a real command). The constraint is too strong for the inert case.Why this matters
Fire-and-forget outbounds are common: thank-you emails, audit notifications, "best effort" pings to systems that don't drive the domain. The event that justified the outbound is already recorded by the time the outbound runs — the outbound just needs to make the network call and log success/failure. No downstream command should be emitted.
Today the consumer has two unappealing options:
ToJSON/NameOfinstances and a no-opdecide. Adds dead command surface, pollutes the command catalogue, and makes the read ofService.hsconfusing ("what is thisNoOpEmailSentcommand and where is it triggered?").Http.Clientfor this integration, calling the underlying HTTP library directly. Drops out of theIntegration.Http.Requestplumbing entirely just to avoid the unsatisfiable constraint.Repro shape
Workaround consumers currently apply
Re-implement the post-then-log-then-no-emit flow against
Http.Client's lower-level primitives, like:This duplicates the work
ToAction (Http.Request command)already does — just so the consumer can stop carrying an inertcommandphantom type around.Suggested fix
Add a
Http.Requestvariant orToActioninstance for the no-command case. Sketch:Option A — sibling instance.
instance ToAction (Http.Request ())(orHttp.Request Void) where the action runs the request, classifies the response, and yieldsNothing(no command emitted). The consumer types its callbacks as\_resp -> ().Option B — explicit fire-and-forget combinator.
Http.fireAndForget :: Http.Request command -> Task IntegrationError (Result Text ()). Skips command-emission unconditionally; thecommandtype-parameter goes unused at runtime but the constraint is dropped from this entry point.Option C — relax the constraint. Drop
(Json.ToJSON command, KnownSymbol (NameOf command))from the request type and move it ontoIntegration.emitCommandinstead, where it actually matters. TheToActioninstance can branch on whetheronSuccess/onErrorreturn()vs a real command (e.g. via aMaybe commandfield).Any of these gives consumers a typed, in-framework way to express "this outbound has no follow-up command" without inventing dead types or escaping to raw HTTP.