all repos

onasty @ dfbb8b461a9edec6e94d3657fd006125040096c9

a one-time notes service
11 files changed, 122 insertions(+), 68 deletions(-)
web: handle api errors (#138)

* web: add decoder for errors

* web: add custom error type for errors form api

* refactor(web): use new Api.Error

* fix(web): apparently the app still handled only Http.Error, and didn't
give a thing about Api.Error

* refactor(web): add helper for getting the error mesasge from all
Api.Error variants

* fixup! refactor(web): add helper for getting the error mesasge from all Api.Error variants

* fixup! refactor(web): add helper for getting the error mesasge from all Api.Error variants
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-20 16:57:47 +0300
Parent: 9de58ad
M web/src/Api.elm

@@ -1,46 +1,31 @@

-module Api exposing (HttpRequestDetails, Response(..), errorToFriendlyMessage) +module Api exposing (Error(..), Response(..), errorMessage) import Http import Json.Decode +type Error + = HttpError + { message : String + , reason : Http.Error + } + | JsonDecodeError + { message : String + , reason : Json.Decode.Error + } + + type Response value = Loading | Success value - | Failure Http.Error + | Failure Error -type alias HttpRequestDetails msg = - { endpoint : String - , method : String - , body : Http.Body - , decoder : Json.Decode.Decoder msg - , onHttpError : Http.Error -> msg - } +errorMessage : Error -> String +errorMessage error = + case error of + HttpError err -> + err.message - -errorToFriendlyMessage : Http.Error -> String -errorToFriendlyMessage httpError = - case httpError of - Http.BadUrl _ -> - "This page requested a bad URL" - - Http.Timeout -> - "Request took too long to respond" - - Http.NetworkError -> - "Could not connect to the API" - - Http.BadStatus code -> - case code of - 404 -> - "Not found" - - 401 -> - "Unauthorized" - - _ -> - "API returned an error code" - - Http.BadBody _ -> - "Unexpected response from API" + JsonDecodeError err -> + err.message
M web/src/Api/Auth.elm

@@ -1,5 +1,6 @@

module Api.Auth exposing (refreshToken, signin, signup) +import Api import Data.Credentials as Credentials exposing (Credentials) import Effect exposing (Effect) import Http

@@ -8,7 +9,7 @@ import Json.Encode as Encode

signin : - { onResponse : Result Http.Error Credentials -> msg + { onResponse : Result Api.Error Credentials -> msg , email : String , password : String }

@@ -32,7 +33,7 @@ }

signup : - { onResponse : Result Http.Error () -> msg + { onResponse : Result Api.Error () -> msg , email : String , password : String }

@@ -56,7 +57,7 @@ }

refreshToken : - { onResponse : Result Http.Error Credentials -> msg + { onResponse : Result Api.Error Credentials -> msg , refreshToken : String } -> Effect msg
M web/src/Api/Me.elm

@@ -1,11 +1,12 @@

module Api.Me exposing (get) +import Api import Data.Me as Me exposing (Me) import Effect exposing (Effect) import Http -get : { onResponse : Result Http.Error Me -> msg } -> Effect msg +get : { onResponse : Result Api.Error Me -> msg } -> Effect msg get options = Effect.sendApiRequest { endpoint = "/api/v1/me"
A web/src/Data/Error.elm

@@ -0,0 +1,13 @@

+module Data.Error exposing (Error, decode) + +import Json.Decode + + +type alias Error = + { message : String } + + +decode : Json.Decode.Decoder Error +decode = + Json.Decode.map Error + (Json.Decode.field "message" Json.Decode.string)
M web/src/Effect.elm

@@ -28,10 +28,11 @@ @docs map, toCmd

-} -import Api exposing (HttpRequestDetails) +import Api import Auth.User import Browser.Navigation import Data.Credentials exposing (Credentials) +import Data.Error import Dict exposing (Dict) import Http import Json.Decode

@@ -59,6 +60,15 @@ -- SHARED

| SendSharedMsg Shared.Msg.Msg | SendToLocalStorage { key : String, value : Json.Encode.Value } | SendApiRequest (HttpRequestDetails msg) + + +type alias HttpRequestDetails msg = + { endpoint : String + , method : String + , body : Http.Body + , decoder : Json.Decode.Decoder msg + , onHttpError : Api.Error -> msg + }

@@ -161,27 +171,25 @@ { endpoint : String

, method : String , body : Http.Body , decoder : Json.Decode.Decoder value - , onResponse : Result Http.Error value -> msg + , onResponse : Result Api.Error value -> msg } -> Effect msg sendApiRequest opts = let - onHttpError : Http.Error -> msg - onHttpError httpError = - opts.onResponse (Err httpError) + onSuccess : value -> msg + onSuccess value = + opts.onResponse (Ok value) - decoder : Json.Decode.Decoder msg - decoder = - opts.decoder - |> Json.Decode.map Ok - |> Json.Decode.map opts.onResponse + onHttpError : Api.Error -> msg + onHttpError err = + opts.onResponse (Err err) in SendApiRequest { endpoint = opts.endpoint , method = opts.method , body = opts.body , onHttpError = onHttpError - , decoder = decoder + , decoder = Json.Decode.map onSuccess opts.decoder }

@@ -326,7 +334,7 @@ , url = opts.endpoint

, headers = headers , body = opts.body , expect = - Http.expectJson + Http.expectStringResponse (\httpResult -> case httpResult of Ok msg ->

@@ -335,7 +343,36 @@

Err err -> opts.onHttpError err ) - opts.decoder + (\resp -> fromHttpResponseToCustomError opts.decoder resp) , timeout = Just (1000 * 60) -- 60 second timeout , tracker = Nothing } + + +fromHttpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg +fromHttpResponseToCustomError decoder response = + case response of + Http.GoodStatus_ _ body -> + case Json.Decode.decodeString decoder body of + Ok data -> + Ok data + + Err err -> + Err (Api.JsonDecodeError { message = "Something unexpected happened", reason = err }) + + Http.BadStatus_ { statusCode } body -> + case Json.Decode.decodeString Data.Error.decode body of + Ok err -> + Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode }) + + Err err -> + Err (Api.JsonDecodeError { message = "Something unexpected happened", reason = err }) + + Http.BadUrl_ url -> + Err (Api.HttpError { message = "Unexpected URL format", reason = Http.BadUrl url }) + + Http.Timeout_ -> + Err (Api.HttpError { message = "Request timed out, please try again", reason = Http.Timeout }) + + Http.NetworkError_ -> + Err (Api.HttpError { message = "Could not connect, please try again", reason = Http.NetworkError })
M web/src/Pages/Auth.elm

@@ -8,7 +8,6 @@ import Effect exposing (Effect)

import Html exposing (Html) import Html.Attributes as Attr import Html.Events -import Http import Layouts import Page exposing (Page) import Route exposing (Route)

@@ -38,7 +37,7 @@ , password : String

, passwordAgain : String , isSubmittingForm : Bool , formVariant : Variant - , error : Maybe Http.Error + , error : Maybe Api.Error }

@@ -71,8 +70,8 @@ type Msg

= UserUpdatedInput Field String | UserChangedFormVariant Variant | UserClickedSubmit - | ApiSignInResponded (Result Http.Error Credentials) - | ApiSignUpResponded (Result Http.Error ()) + | ApiSignInResponded (Result Api.Error Credentials) + | ApiSignUpResponded (Result Api.Error ()) type Field

@@ -198,13 +197,13 @@ ]

) -viewError : Maybe Http.Error -> Html Msg +viewError : Maybe Api.Error -> Html Msg viewError maybeError = case maybeError of Just error -> Html.div [ Attr.class "box bad" ] [ Html.strong [ Attr.class "block titlebar" ] [ Html.text "Error" ] - , Html.text (Api.errorToFriendlyMessage error) + , Html.text (Api.errorMessage error) ] Nothing ->

@@ -230,9 +229,7 @@ viewForgotPassword : Html Msg

viewForgotPassword = Html.div [] [ Html.a - [ Attr.href "/forgot-password" - , Attr.class "gray" - ] + [ Attr.href "/forgot-password" ] [ Html.text "Forgot password?" ] ]
M web/src/Pages/Profile/Me.elm

@@ -6,7 +6,6 @@ import Auth

import Data.Me exposing (Me) import Effect exposing (Effect) import Html exposing (Html) -import Http import Layouts import Page exposing (Page) import Route exposing (Route)

@@ -45,7 +44,7 @@ -- UPDATE

type Msg - = ApiMeResponded (Result Http.Error Me) + = ApiMeResponded (Result Api.Error Me) update : Msg -> Model -> ( Model, Effect Msg )

@@ -88,7 +87,7 @@ Api.Success user ->

viewUserDetails shared user Api.Failure err -> - Html.text (Api.errorToFriendlyMessage err) + Html.text (Api.errorMessage err) viewUserDetails : Shared.Model -> Me -> Html Msg
M web/src/Shared/Msg.elm

@@ -1,7 +1,7 @@

module Shared.Msg exposing (Msg(..)) +import Api import Data.Credentials exposing (Credentials) -import Http import Time

@@ -13,4 +13,4 @@ | SignedIn Credentials

-- Session | CheckTokenExpiration Time.Posix | TriggerTokenRefresh - | ApiRefreshTokensResponded (Result Http.Error Credentials) + | ApiRefreshTokensResponded (Result Api.Error Credentials)
M web/tests/UnitTests/Data/Credentiala.elm

@@ -9,7 +9,7 @@

suite : Test suite = describe "Data.Credentials" - [ test "decode credentials" <| + [ test "decode" <| \_ -> """ {
A web/tests/UnitTests/Data/Error.elm

@@ -0,0 +1,21 @@

+module UnitTests.Data.Error exposing (suite) + +import Data.Error +import Expect +import Json.Decode as Json +import Test exposing (Test, describe, test) + + +suite : Test +suite = + describe "Data.Error" + [ test "decode" <| + \_ -> + """ + { + "message": "some kind of an error" + } + """ + |> Json.decodeString Data.Error.decode + |> Expect.equal (Ok { message = "some kind of an error" }) + ]
M web/tests/UnitTests/Data/Me.elm

@@ -9,7 +9,7 @@

suite : Test suite = describe "Data.Me" - [ test "decode credentials" <| + [ test "decode" <| \_ -> """ {