git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Add ParseResponseBody/BodyEncoding, fix 304 handling, bump to 0.6.3

- ParseResponse: add ParseResponseBody typeclass (String bypasses readJSON, ReadForeign fallback); route all non-2xx through parseErrorRL (fixes 304 Not Modified test) - ParseResponse: add Unit instance for bodyless error responses - MakeRequest: add BodyEncoding typeclass keyed on encoding type (JSON, FormData, NoBody) rather than body type, carrying both content-type and serialisation - ExtractParams: add ExtractBodyEncoding to thread encoding type through the client ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>

commitd2a1d2fdcede82a66a9b1383502943c92dc56f67 authorMark Eibes <mark.eibes@wolt.com> dateThu 21 May 2026, 15:28 (4 months ago) parentea69984
11 files changed+300โˆ’34
package.json+3โˆ’1 spago.yaml+1โˆ’1 src/Yoga/Fetch/Om.purs+6โˆ’5 src/Yoga/Fetch/Om/ExtractParams.purs+13 src/Yoga/Fetch/Om/MakeRequest.js+1 src/Yoga/Fetch/Om/MakeRequest.purs+27โˆ’19 src/Yoga/Fetch/Om/ParseResponse.purs+30โˆ’7 src/Yoga/Fetch/Om/SplitResponses.purs+13โˆ’1 test/BodylessResponse.Example.purs+27 test/BodylessResponse.Spec.purs+98 test/PlainTextResponse.Spec.purs+81
package.json+3โˆ’1
@@ -30,5 +30,7 @@
3030 "spago": "0.93.42",
3131 "vitest": "^2.1.8"
3232 },
33โˆ’ "dependencies": {}
33+ "dependencies": {
34+ "pino": "^10.3.1"
35+ }
3436}
spago.yaml+1โˆ’1
@@ -2,7 +2,7 @@ workspace: {}
22package:
33 name: yoga-fetch-om
44 publish:
5โˆ’ version: 0.6.2
5+ version: 0.6.3
66 license: MIT
77 location:
88 githubOwner: rowtype-yoga
src/Yoga/Fetch/Om.purs+6โˆ’5
@@ -34,8 +34,8 @@ import Yoga.Fetch.Om.BuildUrl (class BuildUrl, buildUrl)
3434import Yoga.Fetch.Om.Simple (class DecodeResponse, decodeResponse, FetchError, FetchResponse, get, getWithHeaders, delete, deleteWithHeaders, delete_, post, postWithHeaders, post_, put, putWithHeaders, put_, patch, patchWithHeaders, patch_)
3535import Yoga.Fetch.Om.StreamDecode (class StreamDecode, decodeStream)
3636import Yoga.Fetch.Om.ClientFunction (class BuildClientFn, class CheckBodyIsUnit, buildClientFn)
37โˆ’import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractRequestHeaders)
38โˆ’import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class SerializeBody, class ContentType, makeRequest, serializeBody, contentType)
37+import Yoga.Fetch.Om.ExtractParams (class ExtractRequestBody, class ExtractBodyEncoding, class ExtractRequestHeaders)
38+import Yoga.Fetch.Om.MakeRequest (class MakeRequest, class BodyEncoding, makeRequest, encodingContentType, encodeBody)
3939import Yoga.Fetch.Om.ParseResponse (class ParseResponse, parseResponse)
4040import Yoga.Fetch.Om.SplitResponses (class SplitResponses)
4141import Yoga.Fetch.Om.Variant (class VariantOrValue, variantOrValue)
@@ -70,12 +70,12 @@ instance
7070 ( SegmentPathParams segments pathParams
7171 , SegmentQueryParams segments queryParams
7272 , ExtractRequestBody request body
73+ , ExtractBodyEncoding request encoding
7374 , ExtractRequestHeaders request headers
7475 , PathPattern segments
7576 , BuildUrl segments pathParams queryParams
7677 , MakeRequest method
77โˆ’ , SerializeBody body
78โˆ’ , ContentType body
78+ , BodyEncoding encoding body
7979 , SplitResponses response successRow routeErrors
8080 , ParseResponse routeErrors successRow
8181 , RowToList successRow successRL
@@ -96,7 +96,8 @@ instance
9696 impl pathQueryRec headersRec bodyVal = do
9797 let url = buildUrl baseUrl (Proxy :: _ segments) pathParamsRec queryParamsRec
9898 let hdrs = toHeaders (Proxy :: _ headersRL) headersRec
99โˆ’ fetchResp <- makeRequest (Proxy :: _ method) url hdrs (contentType (Proxy :: _ body)) (serializeBody bodyVal) # toOm
99+ let ct = encodingContentType (Proxy :: _ encoding)
100+ fetchResp <- makeRequest (Proxy :: _ method) url hdrs ct (encodeBody @encoding bodyVal) # toOm
100101 variant <- parseResponse fetchResp :: Om (Record ()) routeErrors (Variant successRow)
101102 variantOrValue (Proxy :: _ successRL) variant # pure
102103 where
src/Yoga/Fetch/Om/ExtractParams.purs+13
@@ -1,6 +1,7 @@
11module Yoga.Fetch.Om.ExtractParams
22 ( class ExtractRequestHeaders
33 , class ExtractRequestBody
4+ , class ExtractBodyEncoding
45 , class FindBodyType
56 , class UnwrapEncoding
67 , class FindHeaders
@@ -25,6 +26,18 @@ else instance
2526 ExtractRequestBody (Record row) body =>
2627 ExtractRequestBody (Request (Record row)) body
2728
29+class ExtractBodyEncoding :: Type -> Type -> Constraint
30+class ExtractBodyEncoding request encoding | request -> encoding
31+
32+instance
33+ ( RowToList row rl
34+ , FindBodyType rl encoding
35+ ) =>
36+ ExtractBodyEncoding (Record row) encoding
37+else instance
38+ ExtractBodyEncoding (Record row) encoding =>
39+ ExtractBodyEncoding (Request (Record row)) encoding
40+
2841class ExtractRequestHeaders :: Type -> Row Type -> Constraint
2942class ExtractRequestHeaders request headers | request -> headers
3043
src/Yoga/Fetch/Om/MakeRequest.jsnew file+1
@@ -0,0 +1 @@
1+export const toUrlEncoded = (obj) => new URLSearchParams(obj).toString();
src/Yoga/Fetch/Om/MakeRequest.purs+27โˆ’19
@@ -4,8 +4,9 @@ module Yoga.Fetch.Om.MakeRequest
44 , makeRequest
55 , class SerializeBody
66 , serializeBody
7โˆ’ , class ContentType
8โˆ’ , contentType
7+ , class BodyEncoding
8+ , encodingContentType
9+ , encodeBody
910 ) where
1011
1112import Prelude
@@ -31,7 +32,8 @@ import JS.Fetch.Integrity (Integrity(..))
3132import JS.Fetch.Duplex as Duplex
3233import JS.Fetch.Response (Response) as FetchResponse
3334import Promise.Aff as Promise
34โˆ’import Type.Proxy (Proxy)
35+import Type.Proxy (Proxy(..))
36+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
3537import Yoga.HTTP.API.Route.Method as Method
3638import Yoga.JSON (class WriteForeign, writeJSON)
3739
@@ -63,7 +65,7 @@ makeRequest
6365 -> String
6466 -> Maybe String
6567 -> Aff FetchResponse.Response
66โˆ’makeRequest proxy url customHeaders defaultContentType maybeBody = do
68+makeRequest proxy url customHeaders bodyContentType maybeBody = do
6769 request <- Request.new url options # liftEffect
6870 Promise.toAffE $ Fetch.fetch request
6971 where
@@ -73,7 +75,7 @@ makeRequest proxy url customHeaders defaultContentType maybeBody = do
7375 contentTypeArr = case maybeBody of
7476 Nothing -> []
7577 Just _ | hasContentType -> []
76โˆ’ Just _ -> [ "Content-Type" /\ defaultContentType ]
78+ Just _ -> [ "Content-Type" /\ bodyContentType ]
7779 allHeaders = Headers.fromFoldable (contentTypeArr <> customArr)
7880 body = case maybeBody of
7981 Nothing -> Body.empty
@@ -91,24 +93,30 @@ makeRequest proxy url customHeaders defaultContentType maybeBody = do
9193 , cache: Cache.Default
9294 }
9395
94โˆ’class ContentType :: Type -> Constraint
95โˆ’class ContentType body where
96โˆ’ contentType :: Proxy body -> String
97โˆ’
98โˆ’instance ContentType String where
99โˆ’ contentType _ = "text/plain"
100โˆ’else instance ContentType Unit where
101โˆ’ contentType _ = "application/json"
102โˆ’else instance ContentType body where
103โˆ’ contentType _ = "application/json"
104โˆ’
10596class SerializeBody :: Type -> Constraint
10697class SerializeBody body where
10798 serializeBody :: body -> Maybe String
10899
109โˆ’instance SerializeBody String where
110โˆ’ serializeBody s = Just s
111โˆ’else instance SerializeBody Unit where
100+instance SerializeBody Unit where
112101 serializeBody _ = Nothing
113102else instance WriteForeign body => SerializeBody body where
114103 serializeBody b = Just (writeJSON b)
104+
105+class BodyEncoding :: Type -> Type -> Constraint
106+class BodyEncoding encoding body | encoding -> body where
107+ encodingContentType :: Proxy encoding -> String
108+ encodeBody :: body -> Maybe String
109+
110+instance WriteForeign ty => BodyEncoding (JSON ty) ty where
111+ encodingContentType _ = "application/json"
112+ encodeBody b = Just (writeJSON b)
113+
114+else instance BodyEncoding (FormData ty) ty where
115+ encodingContentType _ = "application/x-www-form-urlencoded"
116+ encodeBody b = Just (toUrlEncoded b)
117+
118+else instance BodyEncoding NoBody Unit where
119+ encodingContentType _ = ""
120+ encodeBody _ = Nothing
121+
122+foreign import toUrlEncoded :: forall a. a -> String
src/Yoga/Fetch/Om/ParseResponse.purs+30โˆ’7
@@ -5,6 +5,8 @@ module Yoga.Fetch.Om.ParseResponse
55 , parseSuccessRL
66 , class ParseErrorRL
77 , parseErrorRL
8+ , class ParseResponseBody
9+ , parseResponseBody
810 ) where
911
1012import Prelude
@@ -36,6 +38,15 @@ import Yoga.Om.Error (Exception)
3638import Yoga.Om.Strom (Strom)
3739import Yoga.Om.Strom.WebStream as WebStream
3840
41+class ParseResponseBody (body :: Type) where
42+ parseResponseBody :: String -> Either String body
43+
44+instance ParseResponseBody String where
45+ parseResponseBody = Right
46+
47+else instance ReadForeign body => ParseResponseBody body where
48+ parseResponseBody = withStringErrors <<< readJSON
49+
3950class ParseResponse (errorRow :: Row Type) (successRow :: Row Type) where
4051 parseResponse :: FetchResponse.Response -> Om {} errorRow (Variant successRow)
4152
@@ -50,11 +61,9 @@ instance
5061 let status = Fetch.status fetchResp
5162 if status >= 200 && status < 300 then
5263 parseSuccessRL (Proxy :: _ successRL) status fetchResp # toOm
53โˆ’ else if status >= 400 then do
64+ else do
5465 errorText <- Promise.toAffE (Fetch.text fetchResp) # toOm
5566 parseErrorRL (Proxy :: _ errorRL) (Proxy :: _ errorRow) status errorText
56โˆ’ else
57โˆ’ throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Unexpected HTTP status code: " <> show status)))
5867
5968class ParseSuccessRL (rl :: RowList Type) (successRow :: Row Type) | rl -> successRow where
6069 parseSuccessRL :: Proxy rl -> Int -> FetchResponse.Response -> Aff (Variant successRow)
@@ -100,7 +109,7 @@ else instance
100109else instance
101110 ( IsSymbol label
102111 , StatusCodeMap label
103โˆ’ , ReadForeign body
112+ , ParseResponseBody body
104113 , Row.Cons label body tailRow successRow
105114 , Row.Lacks label tailRow
106115 , ParseSuccessRL tailRL successRow
@@ -110,7 +119,7 @@ else instance
110119 let StatusCode expected = statusCodeFor (Proxy :: _ label)
111120 if actualStatus == expected then do
112121 jsonText <- liftEffect (Fetch.text fetchResp) >>= Promise.toAff
113โˆ’ case withStringErrors (readJSON jsonText) of
122+ case parseResponseBody jsonText of
114123 Left errs -> Aff.throwError (Exception.error ("Failed to parse response (status " <> show actualStatus <> "):\n" <> errs))
115124 Right parsed -> pure (Variant.inj (Proxy :: _ label) parsed)
116125 else
@@ -148,7 +157,21 @@ else instance
148157else instance
149158 ( IsSymbol label
150159 , StatusCodeMap label
151โˆ’ , ReadForeign body
160+ , Row.Cons label Unit tailRow errorRow
161+ , Row.Lacks label tailRow
162+ , Row.Cons label Unit tailExc (Exception errorRow)
163+ , ParseErrorRL tailRL errorRow
164+ ) =>
165+ ParseErrorRL (RL.Cons label Unit tailRL) errorRow where
166+ parseErrorRL _ errorProxy actualStatus jsonText = do
167+ let StatusCode expected = statusCodeFor (Proxy :: _ label)
168+ if actualStatus == expected then throwError (Variant.inj (Proxy :: _ label) unit)
169+ else parseErrorRL (Proxy :: _ tailRL) errorProxy actualStatus jsonText
170+
171+else instance
172+ ( IsSymbol label
173+ , StatusCodeMap label
174+ , ParseResponseBody body
152175 , Row.Cons label body tailRow errorRow
153176 , Row.Lacks label tailRow
154177 , Row.Cons label body tailExc (Exception errorRow)
@@ -157,7 +180,7 @@ else instance
157180 ParseErrorRL (RL.Cons label body tailRL) errorRow where
158181 parseErrorRL _ errorProxy actualStatus jsonText = do
159182 let StatusCode expected = statusCodeFor (Proxy :: _ label)
160โˆ’ if actualStatus == expected then case withStringErrors (readJSON jsonText) of
183+ if actualStatus == expected then case parseResponseBody jsonText of
161184 Left errs -> throwError (Variant.inj (Proxy :: _ "exception") (Exception.error ("Failed to parse error response (status " <> show actualStatus <> "):\n" <> errs)))
162185 Right parsed -> throwError (Variant.inj (Proxy :: _ label) parsed)
163186 else
src/Yoga/Fetch/Om/SplitResponses.purs+13โˆ’1
@@ -1,5 +1,6 @@
11module Yoga.Fetch.Om.SplitResponses
22 ( class IsSuccessStatus
3+ , class ExtractBodyRL
34 , class ExtractBody
45 , class SplitResponses
56 , class SplitResponsesRL
@@ -10,6 +11,7 @@ import Prim.Boolean (True, False)
1011import Prim.Row as Row
1112import Prim.RowList (RowList)
1213import Prim.RowList as RL
14+import Data.Unit (Unit)
1315import Yoga.HTTP.API.Route (Response)
1416
1517class IsSuccessStatus (label :: Symbol) (isSuccess :: Boolean) | label -> isSuccess
@@ -26,12 +28,22 @@ else instance IsSuccessStatus "alreadyReported" True
2628else instance IsSuccessStatus "imUsed" True
2729else instance IsSuccessStatus label False
2830
31+-- Traverse a RowList to find the "body" field; fall back to Unit if absent.
32+class ExtractBodyRL (rl :: RowList Type) (body :: Type) | rl -> body
33+
34+instance ExtractBodyRL RL.Nil Unit
35+
36+instance ExtractBodyRL (RL.Cons "body" body tail) body
37+
38+else instance ExtractBodyRL tail body => ExtractBodyRL (RL.Cons label ty tail) body
39+
2940class ExtractBody (responseType :: Type) (bodyType :: Type) | responseType -> bodyType
3041
3142instance ExtractBody (Response headers body) body
3243
3344else instance
34โˆ’ ( Row.Cons "body" body rest row
45+ ( RL.RowToList row rl
46+ , ExtractBodyRL rl body
3547 ) =>
3648 ExtractBody (Record row) body
3749
test/BodylessResponse.Example.pursnew file+27
@@ -0,0 +1,27 @@
1+-- Compile-time test: bodyless responses (e.g. 304, 401) can use {} without
2+-- wrapping in { body :: {} }.
3+module BodylessResponse.Example where
4+
5+import Prelude
6+
7+import Yoga.Fetch.Om (GET, Route, type (/), client)
8+import Yoga.Om (Om)
9+
10+type Resource = { id :: Int, name :: String }
11+
12+type ResourceAPI =
13+ { getResource ::
14+ Route GET
15+ ("resources" / "id")
16+ {}
17+ ( ok :: { body :: Resource }
18+ , notModified :: {}
19+ , unauthorized :: {}
20+ )
21+ }
22+
23+-- This must typecheck: the derived client exposes notModified/unauthorized as Unit errors.
24+api
25+ :: forall ctx err
26+ . { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
27+api = client @ResourceAPI "https://api.example.com"
test/BodylessResponse.Spec.pursnew file+98
@@ -0,0 +1,98 @@
1+module BodylessResponse.Spec where
2+
3+import Prelude
4+
5+import Effect.Ref (Ref)
6+import Effect.Ref as Ref
7+import Effect.Aff (Aff, bracket)
8+import Effect.Aff.Class (liftAff)
9+import Effect.Class (liftEffect)
10+import Foreign (unsafeToForeign)
11+import Test.Spec (Spec, around, describe, it)
12+import Test.Spec.Assertions (shouldEqual)
13+import Yoga.Fastify.Fastify as F
14+import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15+import Yoga.Fastify.Om (createOmFastify, getOm)
16+import Yoga.Fetch.Om (GET, Route, type (/), client)
17+import Yoga.Om (Om, runOm)
18+
19+type Resource = { id :: Int, name :: String }
20+
21+type ResourceAPI =
22+ { getResource ::
23+ Route GET
24+ ("bodyless-test" / "resource")
25+ {}
26+ ( ok :: { body :: Resource }
27+ , notModified :: {}
28+ , unauthorized :: {}
29+ )
30+ }
31+
32+api :: forall ctx err. { getResource :: Om ctx (notModified :: Unit, unauthorized :: Unit | err) Resource }
33+api = client @ResourceAPI "http://localhost:44932"
34+
35+spec :: Spec Unit
36+spec = around withServer $ describe "bodyless responses (notModified / unauthorized)" do
37+ it "304 Not Modified triggers the notModified handler" \serverMode -> do
38+ liftEffect $ Ref.write "notModified" serverMode
39+ triggered <- liftEffect $ Ref.new false
40+ runOm {}
41+ { exception: \_ -> pure unit
42+ , notModified: \_ -> liftEffect $ Ref.write true triggered
43+ , unauthorized: \_ -> pure unit
44+ }
45+ (void api.getResource)
46+ result <- liftEffect $ Ref.read triggered
47+ liftAff $ result `shouldEqual` true
48+
49+ it "401 Unauthorized triggers the unauthorized handler" \serverMode -> do
50+ liftEffect $ Ref.write "unauthorized" serverMode
51+ triggered <- liftEffect $ Ref.new false
52+ runOm {}
53+ { exception: \_ -> pure unit
54+ , notModified: \_ -> pure unit
55+ , unauthorized: \_ -> liftEffect $ Ref.write true triggered
56+ }
57+ (void api.getResource)
58+ result <- liftEffect $ Ref.read triggered
59+ liftAff $ result `shouldEqual` true
60+
61+ it "200 OK returns the resource body" \serverMode -> do
62+ liftEffect $ Ref.write "ok" serverMode
63+ resource <- runOm {}
64+ { exception: \_ -> pure { id: -1, name: "" }
65+ , notModified: \_ -> pure { id: -1, name: "" }
66+ , unauthorized: \_ -> pure { id: -1, name: "" }
67+ }
68+ api.getResource
69+ liftAff $ resource `shouldEqual` { id: 1, name: "test" }
70+
71+withServer :: forall a. (Ref String -> Aff a) -> Aff a
72+withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
73+ where
74+ acquire = do
75+ serverMode <- liftEffect $ Ref.new "ok"
76+ fastify <- liftEffect do
77+ f <- F.fastify {}
78+ omApp <- createOmFastify {} f
79+ getOm (RouteURL "/bodyless-test/resource") (handler serverMode) omApp
80+ pure f
81+ void $ F.listen { port: Port 44932, host: Host "0.0.0.0" } fastify
82+ pure { fastify, serverMode }
83+
84+ release { fastify } = F.close fastify
85+
86+ handler serverMode reply = do
87+ mode <- liftEffect $ Ref.read serverMode
88+ case mode of
89+ "notModified" -> do
90+ void $ F.status (StatusCode 304) reply # liftEffect
91+ F.send (unsafeToForeign "") reply # liftAff
92+ "unauthorized" -> do
93+ void $ F.status (StatusCode 401) reply # liftEffect
94+ F.send (unsafeToForeign "") reply # liftAff
95+ _ -> do
96+ void $ F.status (StatusCode 200) reply # liftEffect
97+ void $ F.header "content-type" "application/json" reply # liftEffect
98+ F.sendJson (unsafeToForeign { id: 1, name: "test" }) reply # liftAff
test/PlainTextResponse.Spec.pursnew file+81
@@ -0,0 +1,81 @@
1+module PlainTextResponse.Spec where
2+
3+import Prelude
4+
5+import Effect.Aff (Aff, bracket)
6+import Effect.Aff.Class (liftAff)
7+import Effect.Class (liftEffect)
8+import Effect.Ref (Ref)
9+import Effect.Ref as Ref
10+import Foreign (unsafeToForeign)
11+import Test.Spec (Spec, around, describe, it)
12+import Test.Spec.Assertions (shouldEqual)
13+import Yoga.Fastify.Fastify as F
14+import Yoga.Fastify.Fastify (Port(..), Host(..), RouteURL(..), StatusCode(..))
15+import Yoga.Fastify.Om (createOmFastify, postOm)
16+import Yoga.Fetch.Om (POST, Route, type (/), JSON, client)
17+import Yoga.Om (Om, runOm)
18+
19+type Payload = { value :: Int }
20+type Result = { doubled :: Int }
21+
22+type TestAPI =
23+ { double ::
24+ Route POST
25+ ("plain-text-test" / "double")
26+ { body :: JSON Payload }
27+ ( ok :: { body :: Result }
28+ , badRequest :: { body :: String }
29+ )
30+ }
31+
32+api :: { double :: forall ctx err. Payload -> Om ctx (badRequest :: String | err) Result }
33+api = client @TestAPI "http://localhost:44933"
34+
35+spec :: Spec Unit
36+spec = around withServer $ describe "String body on error responses" do
37+ it "200 OK decodes the JSON response body" \_ -> do
38+ result <- runOm {}
39+ { exception: \_ -> pure { doubled: -1 }
40+ , badRequest: \_ -> pure { doubled: -1 }
41+ }
42+ (api.double { value: 21 })
43+ liftAff $ result `shouldEqual` { doubled: 42 }
44+
45+ it "400 badRequest delivers the raw text body as a String" \serverMode -> do
46+ liftEffect $ Ref.write "badRequest" serverMode
47+ captured <- liftEffect $ Ref.new ""
48+ runOm {}
49+ { exception: \_ -> pure unit
50+ , badRequest: \b -> liftEffect $ Ref.write b captured
51+ }
52+ (void $ api.double { value: 0 })
53+ body <- liftEffect $ Ref.read captured
54+ liftAff $ body `shouldEqual` "invalid value"
55+
56+withServer :: forall a. (Ref String -> Aff a) -> Aff a
57+withServer test = bracket acquire release (\{ serverMode } -> test serverMode)
58+ where
59+ acquire = do
60+ serverMode <- liftEffect $ Ref.new "ok"
61+ fastify <- liftEffect do
62+ f <- F.fastify {}
63+ omApp <- createOmFastify {} f
64+ postOm (RouteURL "/plain-text-test/double") (handler serverMode) omApp
65+ pure f
66+ void $ F.listen { port: Port 44933, host: Host "0.0.0.0" } fastify
67+ pure { fastify, serverMode }
68+
69+ release { fastify } = F.close fastify
70+
71+ handler serverMode reply = do
72+ mode <- liftEffect $ Ref.read serverMode
73+ case mode of
74+ "badRequest" -> do
75+ void $ F.status (StatusCode 400) reply # liftEffect
76+ void $ F.header "content-type" "text/plain" reply # liftEffect
77+ F.send (unsafeToForeign "invalid value") reply # liftAff
78+ _ -> do
79+ void $ F.status (StatusCode 200) reply # liftEffect
80+ void $ F.header "content-type" "application/json" reply # liftEffect
81+ F.sendJson (unsafeToForeign { doubled: 42 }) reply # liftAff