mirror of
https://github.com/dillonkearns/elm-pages.git
synced 2024-09-11 09:05:28 +03:00
Use HTTP conditional requests, and fix some handling of HTTP error cases for BackendTask error paths (return the error data instead of handling them as build errors).
This commit is contained in:
parent
fef2cbf6a4
commit
89491a9515
@ -1,5 +1,5 @@
|
||||
it(`glob tests`, () => {
|
||||
cy.visit("/tests");
|
||||
cy.contains("All tests passed");
|
||||
cy.document().should("not.include.text", "Expected");
|
||||
cy.get(".test-pass").should("exist");
|
||||
cy.get(".test-fail").should("not.exist");
|
||||
});
|
||||
|
5
cypress/e2e/http-backend-task.cy.ts
Normal file
5
cypress/e2e/http-backend-task.cy.ts
Normal file
@ -0,0 +1,5 @@
|
||||
it(`BackendTask tests`, () => {
|
||||
cy.visit("/http-tests");
|
||||
cy.get(".test-pass").should("exist");
|
||||
cy.get(".test-fail").should("not.exist");
|
||||
});
|
@ -16,6 +16,7 @@ import Route exposing (Route)
|
||||
import Server.Request as Request
|
||||
import Server.Response as Response exposing (Response)
|
||||
import Test.Glob
|
||||
import Test.HttpRequests
|
||||
import Test.Runner.Html
|
||||
import Time
|
||||
import Xml.Decode
|
||||
@ -43,6 +44,15 @@ routes getStaticRoutes htmlToString =
|
||||
)
|
||||
|> ApiRoute.literal "tests"
|
||||
|> ApiRoute.serverRender
|
||||
, ApiRoute.succeed
|
||||
(Request.succeed
|
||||
(Test.HttpRequests.all
|
||||
|> BackendTask.map viewHtmlResults
|
||||
|> BackendTask.map html
|
||||
)
|
||||
)
|
||||
|> ApiRoute.literal "http-tests"
|
||||
|> ApiRoute.serverRender
|
||||
, requestPrinter
|
||||
, xmlDecoder
|
||||
, multipleContentTypes
|
||||
|
80
examples/end-to-end/src/Test/HttpRequests.elm
Normal file
80
examples/end-to-end/src/Test/HttpRequests.elm
Normal file
@ -0,0 +1,80 @@
|
||||
module Test.HttpRequests exposing (all)
|
||||
|
||||
import BackendTask exposing (BackendTask)
|
||||
import BackendTask.Http
|
||||
import Exception exposing (Catchable)
|
||||
import Expect
|
||||
import Json.Decode as Decode
|
||||
import Test exposing (Test)
|
||||
|
||||
|
||||
all : BackendTask error Test
|
||||
all =
|
||||
[ BackendTask.Http.get "http://httpstat.us/500" (Decode.succeed ())
|
||||
|> test "http 500 error"
|
||||
(\result ->
|
||||
case result of
|
||||
Err error ->
|
||||
case error of
|
||||
BackendTask.Http.BadStatus metadata string ->
|
||||
metadata.statusCode
|
||||
|> Expect.equal 500
|
||||
|
||||
_ ->
|
||||
Expect.fail ("Expected BadStatus, got :" ++ Debug.toString error)
|
||||
|
||||
Ok () ->
|
||||
Expect.fail "Expected HTTP error, got Ok"
|
||||
)
|
||||
, BackendTask.Http.get "http://httpstat.us/404" (Decode.succeed ())
|
||||
|> test "http 404 error"
|
||||
(\result ->
|
||||
case result of
|
||||
Err error ->
|
||||
case error of
|
||||
BackendTask.Http.BadStatus metadata string ->
|
||||
metadata.statusCode
|
||||
|> Expect.equal 404
|
||||
|
||||
_ ->
|
||||
Expect.fail ("Expected BadStatus, got: " ++ Debug.toString error)
|
||||
|
||||
Ok () ->
|
||||
Expect.fail "Expected HTTP error, got Ok"
|
||||
)
|
||||
, BackendTask.Http.get "https://api.github.com/repos/dillonkearns/elm-pages" (Decode.field "stargazers_count" Decode.int)
|
||||
|> test "200 JSON"
|
||||
(\result ->
|
||||
case result of
|
||||
Err error ->
|
||||
Expect.fail ("Expected BadStatus, got: " ++ Debug.toString error)
|
||||
|
||||
Ok count ->
|
||||
Expect.pass
|
||||
)
|
||||
, BackendTask.Http.get "https://api.github.com/repos/dillonkearns/elm-pages" (Decode.field "this-field-doesn't-exist" Decode.int)
|
||||
|> test "JSON decoding error"
|
||||
(\result ->
|
||||
case result of
|
||||
Err (BackendTask.Http.BadBody (Just (Decode.Failure failureString _)) _) ->
|
||||
failureString
|
||||
|> Expect.equal "Expecting an OBJECT with a field named `this-field-doesn't-exist`"
|
||||
|
||||
_ ->
|
||||
Expect.fail ("Expected BadStatus, got: " ++ Debug.toString result)
|
||||
)
|
||||
]
|
||||
|> BackendTask.combine
|
||||
|> BackendTask.map (Test.describe "glob tests")
|
||||
|
||||
|
||||
test : String -> (Result error data -> Expect.Expectation) -> BackendTask (Catchable error) data -> BackendTask noError Test
|
||||
test name assert task =
|
||||
task
|
||||
|> BackendTask.toResult
|
||||
|> BackendTask.map
|
||||
(\result ->
|
||||
Test.test name <|
|
||||
\() ->
|
||||
assert result
|
||||
)
|
@ -1,5 +1,4 @@
|
||||
const path = require("path");
|
||||
const fetch = require("node-fetch");
|
||||
const objectHash = require("object-hash");
|
||||
const kleur = require("kleur");
|
||||
|
||||
@ -44,6 +43,10 @@ function fullPath(portsHash, request, hasFsAccess) {
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
function lookupOrPerform(portsFile, mode, rawRequest, hasFsAccess, useCache) {
|
||||
const fetch = require("make-fetch-happen").defaults({
|
||||
cachePath: "./.elm-pages/http-cache",
|
||||
cache: mode === "build" ? "no-cache" : "default",
|
||||
});
|
||||
const { fs } = require("./request-cache-fs.js")(hasFsAccess);
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const request = toRequest(rawRequest);
|
||||
@ -151,57 +154,53 @@ function lookupOrPerform(portsFile, mode, rawRequest, hasFsAccess, useCache) {
|
||||
console.timeEnd(`fetch ${request.url}`);
|
||||
const expectString = request.headers["elm-pages-internal"];
|
||||
|
||||
if (response.ok || expectString === "ExpectResponse") {
|
||||
let body;
|
||||
let bodyKind;
|
||||
if (expectString === "ExpectJson") {
|
||||
body = await response.json();
|
||||
let body;
|
||||
let bodyKind;
|
||||
if (expectString === "ExpectJson") {
|
||||
try {
|
||||
body = await response.buffer();
|
||||
body = JSON.parse(body.toString("utf-8"));
|
||||
bodyKind = "json";
|
||||
} else if (
|
||||
expectString === "ExpectBytes" ||
|
||||
expectString === "ExpectBytesResponse"
|
||||
) {
|
||||
bodyKind = "bytes";
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
body = Buffer.from(arrayBuffer).toString("base64");
|
||||
} else if (expectString === "ExpectWhatever") {
|
||||
bodyKind = "whatever";
|
||||
body = null;
|
||||
} else if (
|
||||
expectString === "ExpectResponse" ||
|
||||
expectString === "ExpectString"
|
||||
) {
|
||||
} catch (error) {
|
||||
body = body.toString("utf8");
|
||||
bodyKind = "string";
|
||||
body = await response.text();
|
||||
} else {
|
||||
throw `Unexpected expectString ${expectString}`;
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
responsePath,
|
||||
JSON.stringify({
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
statusCode: response.status,
|
||||
body: body,
|
||||
bodyKind,
|
||||
url: response.url,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
);
|
||||
|
||||
resolve({ kind: "cache-response-path", value: responsePath });
|
||||
} else if (
|
||||
expectString === "ExpectBytes" ||
|
||||
expectString === "ExpectBytesResponse"
|
||||
) {
|
||||
body = await response.buffer();
|
||||
try {
|
||||
body = body.toString("base64");
|
||||
bodyKind = "bytes";
|
||||
} catch (e) {
|
||||
body = body.toString("utf8");
|
||||
bodyKind = "string";
|
||||
}
|
||||
} else if (expectString === "ExpectWhatever") {
|
||||
bodyKind = "whatever";
|
||||
body = null;
|
||||
} else if (
|
||||
expectString === "ExpectResponse" ||
|
||||
expectString === "ExpectString"
|
||||
) {
|
||||
bodyKind = "string";
|
||||
body = await response.text();
|
||||
} else {
|
||||
console.log("@@@ request-cache1 bad HTTP response");
|
||||
reject({
|
||||
title: "BackendTask.Http Error",
|
||||
message: `${kleur
|
||||
.yellow()
|
||||
.underline(request.url)} Bad HTTP response ${response.status} ${
|
||||
response.statusText
|
||||
}
|
||||
`,
|
||||
});
|
||||
throw `Unexpected expectString ${expectString}`;
|
||||
}
|
||||
|
||||
resolve({
|
||||
kind: "response-json",
|
||||
value: {
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
statusCode: response.status,
|
||||
body,
|
||||
bodyKind,
|
||||
url: response.url,
|
||||
statusText: response.statusText,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.trace("@@@ request-cache2 HTTP error", error);
|
||||
reject({
|
||||
|
1351
package-lock.json
generated
1351
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -39,6 +39,7 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsesc": "^3.0.2",
|
||||
"kleur": "^4.1.5",
|
||||
"make-fetch-happen": "^11.0.2",
|
||||
"memfs": "^3.4.7",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-fetch": "^2.6.7",
|
||||
@ -51,6 +52,7 @@
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/make-fetch-happen": "^10.0.1",
|
||||
"@types/micromatch": "^4.0.2",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/serve-static": "^1.15.0",
|
||||
@ -77,4 +79,4 @@
|
||||
"bin": {
|
||||
"elm-pages": "generator/src/cli.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ module BackendTask exposing
|
||||
, andMap
|
||||
, map2, map3, map4, map5, map6, map7, map8, map9
|
||||
, catch, throw, mapError, onError
|
||||
, toResult
|
||||
)
|
||||
|
||||
{-| In an `elm-pages` app, each Route Module can define a value `data` which is a `BackendTask` that will be resolved **before** `init` is called. That means it is also available
|
||||
@ -541,3 +542,12 @@ throw : BackendTask (Catchable error) data -> BackendTask Throwable data
|
||||
throw backendTask =
|
||||
backendTask
|
||||
|> onError (Exception.throw >> fail)
|
||||
|
||||
|
||||
{-| -}
|
||||
toResult : BackendTask (Catchable error) data -> BackendTask noError (Result error data)
|
||||
toResult backendTask =
|
||||
backendTask
|
||||
|> catch
|
||||
|> andThen (Ok >> succeed)
|
||||
|> onError (Err >> succeed)
|
||||
|
@ -67,6 +67,7 @@ and describe your use case!
|
||||
-}
|
||||
|
||||
import BackendTask exposing (BackendTask)
|
||||
import Base64
|
||||
import Bytes exposing (Bytes)
|
||||
import Bytes.Decode
|
||||
import Dict exposing (Dict)
|
||||
@ -327,81 +328,139 @@ requestRaw request__ expect =
|
||||
|
||||
Nothing ->
|
||||
--Err (Pages.StaticHttpRequest.UserCalledStaticHttpFail ("INTERNAL ERROR - expected request" ++ request_.url))
|
||||
Err (BadBody ("INTERNAL ERROR - expected request" ++ request_.url))
|
||||
Err (BadBody Nothing ("INTERNAL ERROR - expected request" ++ request_.url))
|
||||
)
|
||||
|> Result.andThen
|
||||
(\(RequestsAndPending.Response maybeResponse body) ->
|
||||
case ( expect, body, maybeResponse ) of
|
||||
( ExpectJson decoder, RequestsAndPending.JsonBody json, _ ) ->
|
||||
json
|
||||
|> Json.Decode.decodeValue decoder
|
||||
|> Result.mapError
|
||||
(\error ->
|
||||
error
|
||||
|> Json.Decode.errorToString
|
||||
|> BadBody
|
||||
)
|
||||
let
|
||||
maybeBadResponse : Maybe Error
|
||||
maybeBadResponse =
|
||||
case maybeResponse of
|
||||
Just response ->
|
||||
if not (response.statusCode >= 200 && response.statusCode < 300) then
|
||||
case body of
|
||||
RequestsAndPending.StringBody s ->
|
||||
BadStatus
|
||||
{ url = response.url
|
||||
, statusCode = response.statusCode
|
||||
, statusText = response.statusText
|
||||
, headers = response.headers
|
||||
}
|
||||
s
|
||||
|> Just
|
||||
|
||||
( ExpectString mapStringFn, RequestsAndPending.StringBody string, _ ) ->
|
||||
string
|
||||
|> mapStringFn
|
||||
|> Ok
|
||||
RequestsAndPending.BytesBody bytes ->
|
||||
BadStatus
|
||||
{ url = response.url
|
||||
, statusCode = response.statusCode
|
||||
, statusText = response.statusText
|
||||
, headers = response.headers
|
||||
}
|
||||
(Base64.fromBytes bytes |> Maybe.withDefault "")
|
||||
|> Just
|
||||
|
||||
( ExpectResponse mapResponse, RequestsAndPending.StringBody asStringBody, Just rawResponse ) ->
|
||||
let
|
||||
asMetadata : Metadata
|
||||
asMetadata =
|
||||
{ url = rawResponse.url
|
||||
, statusCode = rawResponse.statusCode
|
||||
, statusText = rawResponse.statusText
|
||||
, headers = rawResponse.headers
|
||||
}
|
||||
RequestsAndPending.JsonBody value ->
|
||||
BadStatus
|
||||
{ url = response.url
|
||||
, statusCode = response.statusCode
|
||||
, statusText = response.statusText
|
||||
, headers = response.headers
|
||||
}
|
||||
(Encode.encode 0 value)
|
||||
|> Just
|
||||
|
||||
rawResponseToResponse : Response String
|
||||
rawResponseToResponse =
|
||||
if 200 <= rawResponse.statusCode && rawResponse.statusCode < 300 then
|
||||
GoodStatus_ asMetadata asStringBody
|
||||
RequestsAndPending.WhateverBody ->
|
||||
BadStatus
|
||||
{ url = response.url
|
||||
, statusCode = response.statusCode
|
||||
, statusText = response.statusText
|
||||
, headers = response.headers
|
||||
}
|
||||
""
|
||||
|> Just
|
||||
|
||||
else
|
||||
BadStatus_ asMetadata asStringBody
|
||||
in
|
||||
rawResponseToResponse
|
||||
|> mapResponse
|
||||
|> Ok
|
||||
Nothing
|
||||
|
||||
( ExpectBytesResponse mapResponse, RequestsAndPending.BytesBody rawBytesBody, Just rawResponse ) ->
|
||||
let
|
||||
asMetadata : Metadata
|
||||
asMetadata =
|
||||
{ url = rawResponse.url
|
||||
, statusCode = rawResponse.statusCode
|
||||
, statusText = rawResponse.statusText
|
||||
, headers = rawResponse.headers
|
||||
}
|
||||
Nothing ->
|
||||
Nothing
|
||||
in
|
||||
case maybeBadResponse of
|
||||
Just badResponse ->
|
||||
Err badResponse
|
||||
|
||||
rawResponseToResponse : Response Bytes
|
||||
rawResponseToResponse =
|
||||
if 200 <= rawResponse.statusCode && rawResponse.statusCode < 300 then
|
||||
GoodStatus_ asMetadata rawBytesBody
|
||||
Nothing ->
|
||||
case ( expect, body, maybeResponse ) of
|
||||
( ExpectJson decoder, RequestsAndPending.JsonBody json, _ ) ->
|
||||
json
|
||||
|> Json.Decode.decodeValue decoder
|
||||
|> Result.mapError
|
||||
(\error ->
|
||||
error
|
||||
|> Json.Decode.errorToString
|
||||
|> BadBody (Just error)
|
||||
)
|
||||
|
||||
else
|
||||
BadStatus_ asMetadata rawBytesBody
|
||||
in
|
||||
rawResponseToResponse
|
||||
|> mapResponse
|
||||
|> Ok
|
||||
( ExpectString mapStringFn, RequestsAndPending.StringBody string, _ ) ->
|
||||
string
|
||||
|> mapStringFn
|
||||
|> Ok
|
||||
|
||||
( ExpectBytes bytesDecoder, RequestsAndPending.BytesBody rawBytes, _ ) ->
|
||||
rawBytes
|
||||
|> Bytes.Decode.decode bytesDecoder
|
||||
|> Result.fromMaybe
|
||||
(BadBody "Bytes decoding failed.")
|
||||
( ExpectResponse mapResponse, RequestsAndPending.StringBody asStringBody, Just rawResponse ) ->
|
||||
let
|
||||
asMetadata : Metadata
|
||||
asMetadata =
|
||||
{ url = rawResponse.url
|
||||
, statusCode = rawResponse.statusCode
|
||||
, statusText = rawResponse.statusText
|
||||
, headers = rawResponse.headers
|
||||
}
|
||||
|
||||
( ExpectWhatever whateverValue, RequestsAndPending.WhateverBody, _ ) ->
|
||||
Ok whateverValue
|
||||
rawResponseToResponse : Response String
|
||||
rawResponseToResponse =
|
||||
if 200 <= rawResponse.statusCode && rawResponse.statusCode < 300 then
|
||||
GoodStatus_ asMetadata asStringBody
|
||||
|
||||
_ ->
|
||||
Err (BadBody "Unexpected combination, internal error")
|
||||
else
|
||||
BadStatus_ asMetadata asStringBody
|
||||
in
|
||||
rawResponseToResponse
|
||||
|> mapResponse
|
||||
|> Ok
|
||||
|
||||
( ExpectBytesResponse mapResponse, RequestsAndPending.BytesBody rawBytesBody, Just rawResponse ) ->
|
||||
let
|
||||
asMetadata : Metadata
|
||||
asMetadata =
|
||||
{ url = rawResponse.url
|
||||
, statusCode = rawResponse.statusCode
|
||||
, statusText = rawResponse.statusText
|
||||
, headers = rawResponse.headers
|
||||
}
|
||||
|
||||
rawResponseToResponse : Response Bytes
|
||||
rawResponseToResponse =
|
||||
if 200 <= rawResponse.statusCode && rawResponse.statusCode < 300 then
|
||||
GoodStatus_ asMetadata rawBytesBody
|
||||
|
||||
else
|
||||
BadStatus_ asMetadata rawBytesBody
|
||||
in
|
||||
rawResponseToResponse
|
||||
|> mapResponse
|
||||
|> Ok
|
||||
|
||||
( ExpectBytes bytesDecoder, RequestsAndPending.BytesBody rawBytes, _ ) ->
|
||||
rawBytes
|
||||
|> Bytes.Decode.decode bytesDecoder
|
||||
|> Result.fromMaybe
|
||||
(BadBody Nothing "Bytes decoding failed.")
|
||||
|
||||
( ExpectWhatever whateverValue, RequestsAndPending.WhateverBody, _ ) ->
|
||||
Ok whateverValue
|
||||
|
||||
_ ->
|
||||
Err (BadBody Nothing "Unexpected combination, internal error")
|
||||
)
|
||||
|> BackendTask.fromResult
|
||||
|> BackendTask.mapError
|
||||
@ -432,7 +491,7 @@ errorToString error =
|
||||
[ TerminalText.text ("BadStatus: " ++ string)
|
||||
]
|
||||
|
||||
BadBody string ->
|
||||
BadBody _ string ->
|
||||
[ TerminalText.text ("BadBody: " ++ string)
|
||||
]
|
||||
)
|
||||
@ -464,4 +523,4 @@ type Error
|
||||
| Timeout
|
||||
| NetworkError
|
||||
| BadStatus Metadata String
|
||||
| BadBody String
|
||||
| BadBody (Maybe Json.Decode.Error) String
|
||||
|
Loading…
Reference in New Issue
Block a user