Skip to content

Commit 30f8ffb

Browse files
committed
Capture response format decode failures in the response
Previously this would have caused the whole request to fail, obscuring any kind of response information we did receive (status code, etc).
1 parent 20bc9b4 commit 30f8ffb

File tree

6 files changed

+71
-40
lines changed

6 files changed

+71
-40
lines changed

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ import Data.Argonaut.Core as J
3434
import Data.Either (Either(..))
3535
import Data.HTTP.Method (Method(..))
3636
import Effect.Aff (launchAff)
37-
import Effect.Class (liftEffect)
38-
import Effect.Console (log)
37+
import Effect.Class.Console (log)
3938
4039
main = launchAff $ do
4140
res <- AX.request ResponseFormat.json (AX.defaultRequest { url = "/api", method = Left GET })
42-
liftEffect $ log $ "GET /api response: " <> J.stringify res.response
41+
case res.body of
42+
Left err -> log $ "GET /api response failed to decode: " <> AX.printResponseFormatError err
43+
Right json -> log $ "GET /api response: " <> J.stringify json
4344
```
4445

4546
(`defaultRequest` is a record value that has all the required fields pre-set for convenient overriding when making a request.)
@@ -51,10 +52,14 @@ import Affjax.RequestBody as RequestBody
5152
5253
main = launchAff $ do
5354
res1 <- AX.get ResponseFormat.json "/api"
54-
liftEffect $ log $ "GET /api response: " <> J.stringify res1.response
55+
case res1.body of
56+
Left err -> log $ "GET /api response failed to decode: " <> AX.printResponseFormatError err
57+
Right json -> log $ "GET /api response: " <> J.stringify json
5558
5659
res2 <- AX.post ResponseFormat.json "/api" (RequestBody.json (J.fromString "test"))
57-
liftEffect $ log $ "POST /api response: " <> J.stringify res2.response
60+
case res2.body of
61+
Left err -> log $ "POST /api response failed to decode: " <> AX.printResponseFormatError err
62+
Right json -> log $ "POST /api response: " <> J.stringify json
5863
```
5964

6065
See the module documentation for a full list of these helpers.

src/Affjax.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ exports._ajax = function () {
6868
var i = header.indexOf(":");
6969
return mkHeader(header.substring(0, i))(header.substring(i + 2));
7070
}),
71-
response: platformSpecific.getResponse(xhr)
71+
body: platformSpecific.getResponse(xhr)
7272
});
7373
};
7474
xhr.responseType = options.responseType;

src/Affjax.purs

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ module Affjax
1212
, RetryPolicy(..)
1313
, defaultRetryPolicy
1414
, retry
15+
, module Affjax.ResponseFormat
1516
) where
1617

1718
import Prelude
1819

1920
import Affjax.RequestBody as RequestBody
2021
import Affjax.RequestHeader (RequestHeader(..), requestHeaderName, requestHeaderValue)
22+
import Affjax.ResponseFormat (ResponseFormatError(..), printResponseFormatError)
2123
import Affjax.ResponseFormat as ResponseFormat
2224
import Affjax.ResponseHeader (ResponseHeader, responseHeader)
2325
import Affjax.StatusCode (StatusCode(..))
@@ -26,7 +28,6 @@ import Control.Parallel (parOneOf)
2628
import Data.Argonaut.Core (Json)
2729
import Data.Argonaut.Core as J
2830
import Data.Argonaut.Parser (jsonParser)
29-
import Data.Array (intercalate)
3031
import Data.Array as Arr
3132
import Data.Either (Either(..), either)
3233
import Data.Foldable (any)
@@ -36,6 +37,7 @@ import Data.Function.Uncurried (Fn2, runFn2)
3637
import Data.HTTP.Method (Method(..), CustomMethod)
3738
import Data.HTTP.Method as Method
3839
import Data.Int (toNumber)
40+
import Data.List.NonEmpty as NEL
3941
import Data.Maybe (Maybe(..))
4042
import Data.Nullable (Nullable, toNullable)
4143
import Data.Time.Duration (Milliseconds(..))
@@ -44,7 +46,7 @@ import Effect.Aff.Compat as AC
4446
import Effect.Class (liftEffect)
4547
import Effect.Exception (Error, error)
4648
import Effect.Ref as Ref
47-
import Foreign (F, Foreign, ForeignError(..), fail, renderForeignError, unsafeReadTagged, unsafeToForeign)
49+
import Foreign (F, Foreign, ForeignError(..), fail, unsafeReadTagged, unsafeToForeign)
4850
import Math as Math
4951

5052
-- | A record that contains all the information to perform an HTTP request.
@@ -87,77 +89,77 @@ type Response a =
8789
{ status :: StatusCode
8890
, statusText :: String
8991
, headers :: Array ResponseHeader
90-
, response :: a
92+
, body :: a
9193
}
9294

9395
-- | Type alias for URL strings to aid readability of types.
9496
type URL = String
9597

9698
-- | Makes a `GET` request to the specified URL.
97-
get :: forall a. ResponseFormat.ResponseFormat a -> URL -> Aff (Response a)
99+
get :: forall a. ResponseFormat.ResponseFormat a -> URL -> Aff (Response (Either ResponseFormatError a))
98100
get rt u = request rt $ defaultRequest { url = u }
99101

100102
-- | Makes a `POST` request to the specified URL, sending data.
101-
post :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Response a)
103+
post :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Response (Either ResponseFormatError a))
102104
post rt u c = request rt $ defaultRequest { method = Left POST, url = u, content = Just c }
103105

104106
-- | Makes a `POST` request to the specified URL with the option to send data.
105-
post' :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Response a)
107+
post' :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Response (Either ResponseFormatError a))
106108
post' rt u c = request rt $ defaultRequest { method = Left POST, url = u, content = c }
107109

108110
-- | Makes a `POST` request to the specified URL, sending data and ignoring the
109111
-- | response.
110112
post_ :: URL -> RequestBody.RequestBody -> Aff (Response Unit)
111-
post_ = post ResponseFormat.ignore
113+
post_ url = map (_ { body = unit }) <<< post ResponseFormat.ignore url
112114

113115
-- | Makes a `POST` request to the specified URL with the option to send data,
114116
-- | and ignores the response.
115117
post_' :: URL -> Maybe RequestBody.RequestBody -> Aff (Response Unit)
116-
post_' = post' ResponseFormat.ignore
118+
post_' url = map (_ { body = unit }) <<< post' ResponseFormat.ignore url
117119

118120
-- | Makes a `PUT` request to the specified URL, sending data.
119-
put :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Response a)
121+
put :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Response (Either ResponseFormatError a))
120122
put rt u c = request rt $ defaultRequest { method = Left PUT, url = u, content = Just c }
121123

122124
-- | Makes a `PUT` request to the specified URL with the option to send data.
123-
put' :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Response a)
125+
put' :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Response (Either ResponseFormatError a))
124126
put' rt u c = request rt $ defaultRequest { method = Left PUT, url = u, content = c }
125127

126128
-- | Makes a `PUT` request to the specified URL, sending data and ignoring the
127129
-- | response.
128130
put_ :: URL -> RequestBody.RequestBody -> Aff (Response Unit)
129-
put_ = put ResponseFormat.ignore
131+
put_ url = map (_ { body = unit }) <<< put ResponseFormat.ignore url
130132

131133
-- | Makes a `PUT` request to the specified URL with the option to send data,
132134
-- | and ignores the response.
133135
put_' :: URL -> Maybe RequestBody.RequestBody -> Aff (Response Unit)
134-
put_' = put' ResponseFormat.ignore
136+
put_' url = map (_ { body = unit }) <<< put' ResponseFormat.ignore url
135137

136138
-- | Makes a `DELETE` request to the specified URL.
137-
delete :: forall a. ResponseFormat.ResponseFormat a -> URL -> Aff (Response a)
139+
delete :: forall a. ResponseFormat.ResponseFormat a -> URL -> Aff (Response (Either ResponseFormatError a))
138140
delete rt u = request rt $ defaultRequest { method = Left DELETE, url = u }
139141

140142
-- | Makes a `DELETE` request to the specified URL and ignores the response.
141143
delete_ :: URL -> Aff (Response Unit)
142-
delete_ = delete ResponseFormat.ignore
144+
delete_ = map (_ { body = unit }) <<< delete ResponseFormat.ignore
143145

144146
-- | Makes a `PATCH` request to the specified URL, sending data.
145-
patch :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Response a)
147+
patch :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Response (Either ResponseFormatError a))
146148
patch rt u c = request rt $ defaultRequest { method = Left PATCH, url = u, content = Just c }
147149

148150
-- | Makes a `PATCH` request to the specified URL with the option to send data.
149-
patch' :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Response a)
151+
patch' :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Response (Either ResponseFormatError a))
150152
patch' rt u c = request rt $ defaultRequest { method = Left PATCH, url = u, content = c }
151153

152154
-- | Makes a `PATCH` request to the specified URL, sending data and ignoring the
153155
-- | response.
154156
patch_ :: URL -> RequestBody.RequestBody -> Aff (Response Unit)
155-
patch_ = patch ResponseFormat.ignore
157+
patch_ url = map (_ { body = unit }) <<< patch ResponseFormat.ignore url
156158

157159
-- | Makes a `PATCH` request to the specified URL with the option to send data,
158160
-- | and ignores the response.
159161
patch_' :: URL -> Maybe RequestBody.RequestBody -> Aff (Response Unit)
160-
patch_' = patch' ResponseFormat.ignore
162+
patch_' url = map (_ { body = unit }) <<< patch' ResponseFormat.ignore url
161163

162164
-- | A sequence of retry delays, in milliseconds.
163165
type RetryDelayCurve = Int -> Milliseconds
@@ -236,12 +238,14 @@ retry policy run req = do
236238
-- | ```purescript
237239
-- | get json "/resource"
238240
-- | ```
239-
request :: forall a. ResponseFormat.ResponseFormat a -> RequestOptions -> Aff (Response a)
241+
request :: forall a. ResponseFormat.ResponseFormat a -> RequestOptions -> Aff (Response (Either ResponseFormatError a))
240242
request rt req = do
241243
res <- AC.fromEffectFnAff $ runFn2 _ajax responseHeader req'
242-
case runExcept (fromResponse' res.response) of
243-
Left err -> throwError $ error $ intercalate "\n" (map renderForeignError err)
244-
Right res' -> pure (res { response = res' })
244+
case runExcept (fromResponse' res.body) of
245+
Left err -> do
246+
pure (res { body = Left (ResponseFormatError (NEL.head err) res.body) })
247+
Right res' -> do
248+
pure (res { body = Right res' })
245249
where
246250

247251
req' :: AjaxRequest a

src/Affjax/ResponseFormat.purs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Data.ArrayBuffer.Types (ArrayBuffer)
77
import Data.Maybe (Maybe(..))
88
import Data.MediaType (MediaType)
99
import Data.MediaType.Common (applicationJSON)
10+
import Foreign (Foreign, ForeignError)
11+
import Foreign as Foreign
1012
import Web.DOM.Document (Document)
1113
import Web.File.Blob (Blob)
1214

@@ -54,3 +56,12 @@ toMediaType =
5456
case _ of
5557
Json _ -> Just applicationJSON
5658
_ -> Nothing
59+
60+
-- | Used when an error occurs when attempting to decode into a particular
61+
-- | response format. The error that occurred when decoding is included, along
62+
-- | with the value that decoding was attempted on.
63+
data ResponseFormatError = ResponseFormatError ForeignError Foreign
64+
65+
printResponseFormatError :: ResponseFormatError String
66+
printResponseFormatError (ResponseFormatError err _) =
67+
Foreign.renderForeignError err

test/DocExamples.purs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,25 @@ import Affjax.ResponseFormat as ResponseFormat
88
import Data.Argonaut.Core as J
99
import Data.Either (Either(..))
1010
import Data.HTTP.Method (Method(..))
11+
import Effect (Effect)
1112
import Effect.Aff (launchAff)
12-
import Effect.Class (liftEffect)
13-
import Effect.Console (log)
13+
import Effect.Class.Console (log)
1414

15-
main = launchAff $ do
15+
main :: Effect Unit
16+
main = void $ launchAff $ do
1617
res <- AX.request ResponseFormat.json (AX.defaultRequest { url = "/api", method = Left GET })
17-
liftEffect $ log $ "GET /api response: " <> J.stringify res.response
18+
case res.body of
19+
Left err -> log $ "GET /api response failed to decode: " <> AX.printResponseFormatError err
20+
Right json -> log $ "GET /api response: " <> J.stringify json
1821

19-
main' = launchAff $ do
22+
main' :: Effect Unit
23+
main' = void $ launchAff $ do
2024
res1 <- AX.get ResponseFormat.json "/api"
21-
liftEffect $ log $ "GET /api response: " <> J.stringify res1.response
25+
case res1.body of
26+
Left err -> log $ "GET /api response failed to decode: " <> AX.printResponseFormatError err
27+
Right json -> log $ "GET /api response: " <> J.stringify json
2228

2329
res2 <- AX.post ResponseFormat.json "/api" (RequestBody.json (J.fromString "test"))
24-
liftEffect $ log $ "POST /api response: " <> J.stringify res2.response
30+
case res2.body of
31+
Left err -> log $ "POST /api response failed to decode: " <> AX.printResponseFormatError err
32+
Right json -> log $ "POST /api response: " <> J.stringify json

test/Main.purs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ main = void $ runAff (either (\e -> logShow e *> throwException e) (const $ log
7878
(attempt $ AX.request ResponseFormat.ignore $ AX.defaultRequest { url = doesNotExist }) >>= assertRight >>= \res -> do
7979
assertEq notFound404 res.status
8080

81-
A.log "GET /not-json: invalid JSON with Foreign response should throw an error"
82-
void $ assertLeft =<< attempt (AX.get ResponseFormat.json doesNotExist)
81+
A.log "GET /not-json: invalid JSON with Foreign response should return an error"
82+
attempt (AX.get ResponseFormat.json doesNotExist) >>= assertRight >>= \res -> do
83+
void $ assertLeft res.body
8384

8485
A.log "GET /not-json: invalid JSON with String response should be ok"
8586
(attempt $ AX.get ResponseFormat.string notJson) >>= assertRight >>= \res -> do
@@ -88,14 +89,16 @@ main = void $ runAff (either (\e -> logShow e *> throwException e) (const $ log
8889
A.log "POST /mirror: should use the POST method"
8990
(attempt $ AX.post ResponseFormat.json mirror (RequestBody.string "test")) >>= assertRight >>= \res -> do
9091
assertEq ok200 res.status
91-
assertEq (Just "POST") (J.toString =<< FO.lookup "method" =<< J.toObject res.response)
92+
json <- assertRight res.body
93+
assertEq (Just "POST") (J.toString =<< FO.lookup "method" =<< J.toObject json)
9294

9395
A.log "PUT with a request body"
9496
let content = "the quick brown fox jumps over the lazy dog"
9597
(attempt $ AX.put ResponseFormat.json mirror (RequestBody.string content)) >>= assertRight >>= \res -> do
9698
assertEq ok200 res.status
97-
assertEq (Just "PUT") (J.toString =<< FO.lookup "method" =<< J.toObject res.response)
98-
assertEq (Just content) (J.toString =<< FO.lookup "body" =<< J.toObject res.response)
99+
json <- assertRight res.body
100+
assertEq (Just "PUT") (J.toString =<< FO.lookup "method" =<< J.toObject json)
101+
assertEq (Just content) (J.toString =<< FO.lookup "body" =<< J.toObject json)
99102

100103
A.log "Testing CORS, HTTPS"
101104
(attempt $ AX.get ResponseFormat.json "https://cors-test.appspot.com/test") >>= assertRight >>= \res -> do

0 commit comments

Comments
 (0)