all repos

onasty @ dfbb8b4

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
        
        -module Api exposing (HttpRequestDetails, Response(..), errorToFriendlyMessage)

      
        
        1
        +module Api exposing (Error(..), Response(..), errorMessage)

      
        2
        2
         

      
        3
        3
         import Http

      
        4
        4
         import Json.Decode

      
        5
        5
         

      
        6
        6
         

      
        
        7
        +type Error

      
        
        8
        +    = HttpError

      
        
        9
        +        { message : String

      
        
        10
        +        , reason : Http.Error

      
        
        11
        +        }

      
        
        12
        +    | JsonDecodeError

      
        
        13
        +        { message : String

      
        
        14
        +        , reason : Json.Decode.Error

      
        
        15
        +        }

      
        
        16
        +

      
        
        17
        +

      
        7
        18
         type Response value

      
        8
        19
             = Loading

      
        9
        20
             | Success value

      
        10
        
        -    | Failure Http.Error

      
        
        21
        +    | Failure Error

      
        11
        22
         

      
        12
        23
         

      
        13
        
        -type alias HttpRequestDetails msg =

      
        14
        
        -    { endpoint : String

      
        15
        
        -    , method : String

      
        16
        
        -    , body : Http.Body

      
        17
        
        -    , decoder : Json.Decode.Decoder msg

      
        18
        
        -    , onHttpError : Http.Error -> msg

      
        19
        
        -    }

      
        
        24
        +errorMessage : Error -> String

      
        
        25
        +errorMessage error =

      
        
        26
        +    case error of

      
        
        27
        +        HttpError err ->

      
        
        28
        +            err.message

      
        20
        29
         

      
        21
        
        -

      
        22
        
        -errorToFriendlyMessage : Http.Error -> String

      
        23
        
        -errorToFriendlyMessage httpError =

      
        24
        
        -    case httpError of

      
        25
        
        -        Http.BadUrl _ ->

      
        26
        
        -            "This page requested a bad URL"

      
        27
        
        -

      
        28
        
        -        Http.Timeout ->

      
        29
        
        -            "Request took too long to respond"

      
        30
        
        -

      
        31
        
        -        Http.NetworkError ->

      
        32
        
        -            "Could not connect to the API"

      
        33
        
        -

      
        34
        
        -        Http.BadStatus code ->

      
        35
        
        -            case code of

      
        36
        
        -                404 ->

      
        37
        
        -                    "Not found"

      
        38
        
        -

      
        39
        
        -                401 ->

      
        40
        
        -                    "Unauthorized"

      
        41
        
        -

      
        42
        
        -                _ ->

      
        43
        
        -                    "API returned an error code"

      
        44
        
        -

      
        45
        
        -        Http.BadBody _ ->

      
        46
        
        -            "Unexpected response from API"

      
        
        30
        +        JsonDecodeError err ->

      
        
        31
        +            err.message

      
M web/src/Api/Auth.elm
···
        1
        1
         module Api.Auth exposing (refreshToken, signin, signup)

      
        2
        2
         

      
        
        3
        +import Api

      
        3
        4
         import Data.Credentials as Credentials exposing (Credentials)

      
        4
        5
         import Effect exposing (Effect)

      
        5
        6
         import Http

      ···
        8
        9
         

      
        9
        10
         

      
        10
        11
         signin :

      
        11
        
        -    { onResponse : Result Http.Error Credentials -> msg

      
        
        12
        +    { onResponse : Result Api.Error Credentials -> msg

      
        12
        13
             , email : String

      
        13
        14
             , password : String

      
        14
        15
             }

      ···
        32
        33
         

      
        33
        34
         

      
        34
        35
         signup :

      
        35
        
        -    { onResponse : Result Http.Error () -> msg

      
        
        36
        +    { onResponse : Result Api.Error () -> msg

      
        36
        37
             , email : String

      
        37
        38
             , password : String

      
        38
        39
             }

      ···
        56
        57
         

      
        57
        58
         

      
        58
        59
         refreshToken :

      
        59
        
        -    { onResponse : Result Http.Error Credentials -> msg

      
        
        60
        +    { onResponse : Result Api.Error Credentials -> msg

      
        60
        61
             , refreshToken : String

      
        61
        62
             }

      
        62
        63
             -> Effect msg

      
M web/src/Api/Me.elm
···
        1
        1
         module Api.Me exposing (get)

      
        2
        2
         

      
        
        3
        +import Api

      
        3
        4
         import Data.Me as Me exposing (Me)

      
        4
        5
         import Effect exposing (Effect)

      
        5
        6
         import Http

      
        6
        7
         

      
        7
        8
         

      
        8
        
        -get : { onResponse : Result Http.Error Me -> msg } -> Effect msg

      
        
        9
        +get : { onResponse : Result Api.Error Me -> msg } -> Effect msg

      
        9
        10
         get options =

      
        10
        11
             Effect.sendApiRequest

      
        11
        12
                 { endpoint = "/api/v1/me"

      
A web/src/Data/Error.elm
···
        
        1
        +module Data.Error exposing (Error, decode)

      
        
        2
        +

      
        
        3
        +import Json.Decode

      
        
        4
        +

      
        
        5
        +

      
        
        6
        +type alias Error =

      
        
        7
        +    { message : String }

      
        
        8
        +

      
        
        9
        +

      
        
        10
        +decode : Json.Decode.Decoder Error

      
        
        11
        +decode =

      
        
        12
        +    Json.Decode.map Error

      
        
        13
        +        (Json.Decode.field "message" Json.Decode.string)

      
M web/src/Effect.elm
···
        28
        28
         

      
        29
        29
         -}

      
        30
        30
         

      
        31
        
        -import Api exposing (HttpRequestDetails)

      
        
        31
        +import Api

      
        32
        32
         import Auth.User

      
        33
        33
         import Browser.Navigation

      
        34
        34
         import Data.Credentials exposing (Credentials)

      
        
        35
        +import Data.Error

      
        35
        36
         import Dict exposing (Dict)

      
        36
        37
         import Http

      
        37
        38
         import Json.Decode

      ···
        59
        60
             | SendSharedMsg Shared.Msg.Msg

      
        60
        61
             | SendToLocalStorage { key : String, value : Json.Encode.Value }

      
        61
        62
             | SendApiRequest (HttpRequestDetails msg)

      
        
        63
        +

      
        
        64
        +

      
        
        65
        +type alias HttpRequestDetails msg =

      
        
        66
        +    { endpoint : String

      
        
        67
        +    , method : String

      
        
        68
        +    , body : Http.Body

      
        
        69
        +    , decoder : Json.Decode.Decoder msg

      
        
        70
        +    , onHttpError : Api.Error -> msg

      
        
        71
        +    }

      
        62
        72
         

      
        63
        73
         

      
        64
        74
         

      ···
        161
        171
             , method : String

      
        162
        172
             , body : Http.Body

      
        163
        173
             , decoder : Json.Decode.Decoder value

      
        164
        
        -    , onResponse : Result Http.Error value -> msg

      
        
        174
        +    , onResponse : Result Api.Error value -> msg

      
        165
        175
             }

      
        166
        176
             -> Effect msg

      
        167
        177
         sendApiRequest opts =

      
        168
        178
             let

      
        169
        
        -        onHttpError : Http.Error -> msg

      
        170
        
        -        onHttpError httpError =

      
        171
        
        -            opts.onResponse (Err httpError)

      
        
        179
        +        onSuccess : value -> msg

      
        
        180
        +        onSuccess value =

      
        
        181
        +            opts.onResponse (Ok value)

      
        172
        182
         

      
        173
        
        -        decoder : Json.Decode.Decoder msg

      
        174
        
        -        decoder =

      
        175
        
        -            opts.decoder

      
        176
        
        -                |> Json.Decode.map Ok

      
        177
        
        -                |> Json.Decode.map opts.onResponse

      
        
        183
        +        onHttpError : Api.Error -> msg

      
        
        184
        +        onHttpError err =

      
        
        185
        +            opts.onResponse (Err err)

      
        178
        186
             in

      
        179
        187
             SendApiRequest

      
        180
        188
                 { endpoint = opts.endpoint

      
        181
        189
                 , method = opts.method

      
        182
        190
                 , body = opts.body

      
        183
        191
                 , onHttpError = onHttpError

      
        184
        
        -        , decoder = decoder

      
        
        192
        +        , decoder = Json.Decode.map onSuccess opts.decoder

      
        185
        193
                 }

      
        186
        194
         

      
        187
        195
         

      ···
        326
        334
                         , headers = headers

      
        327
        335
                         , body = opts.body

      
        328
        336
                         , expect =

      
        329
        
        -                    Http.expectJson

      
        
        337
        +                    Http.expectStringResponse

      
        330
        338
                                 (\httpResult ->

      
        331
        339
                                     case httpResult of

      
        332
        340
                                         Ok msg ->

      ···
        335
        343
                                         Err err ->

      
        336
        344
                                             opts.onHttpError err

      
        337
        345
                                 )

      
        338
        
        -                        opts.decoder

      
        
        346
        +                        (\resp -> fromHttpResponseToCustomError opts.decoder resp)

      
        339
        347
                         , timeout = Just (1000 * 60) -- 60 second timeout

      
        340
        348
                         , tracker = Nothing

      
        341
        349
                         }

      
        
        350
        +

      
        
        351
        +

      
        
        352
        +fromHttpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg

      
        
        353
        +fromHttpResponseToCustomError decoder response =

      
        
        354
        +    case response of

      
        
        355
        +        Http.GoodStatus_ _ body ->

      
        
        356
        +            case Json.Decode.decodeString decoder body of

      
        
        357
        +                Ok data ->

      
        
        358
        +                    Ok data

      
        
        359
        +

      
        
        360
        +                Err err ->

      
        
        361
        +                    Err (Api.JsonDecodeError { message = "Something unexpected happened", reason = err })

      
        
        362
        +

      
        
        363
        +        Http.BadStatus_ { statusCode } body ->

      
        
        364
        +            case Json.Decode.decodeString Data.Error.decode body of

      
        
        365
        +                Ok err ->

      
        
        366
        +                    Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode })

      
        
        367
        +

      
        
        368
        +                Err err ->

      
        
        369
        +                    Err (Api.JsonDecodeError { message = "Something unexpected happened", reason = err })

      
        
        370
        +

      
        
        371
        +        Http.BadUrl_ url ->

      
        
        372
        +            Err (Api.HttpError { message = "Unexpected URL format", reason = Http.BadUrl url })

      
        
        373
        +

      
        
        374
        +        Http.Timeout_ ->

      
        
        375
        +            Err (Api.HttpError { message = "Request timed out, please try again", reason = Http.Timeout })

      
        
        376
        +

      
        
        377
        +        Http.NetworkError_ ->

      
        
        378
        +            Err (Api.HttpError { message = "Could not connect, please try again", reason = Http.NetworkError })

      
M web/src/Pages/Auth.elm
···
        8
        8
         import Html exposing (Html)

      
        9
        9
         import Html.Attributes as Attr

      
        10
        10
         import Html.Events

      
        11
        
        -import Http

      
        12
        11
         import Layouts

      
        13
        12
         import Page exposing (Page)

      
        14
        13
         import Route exposing (Route)

      ···
        38
        37
             , passwordAgain : String

      
        39
        38
             , isSubmittingForm : Bool

      
        40
        39
             , formVariant : Variant

      
        41
        
        -    , error : Maybe Http.Error

      
        
        40
        +    , error : Maybe Api.Error

      
        42
        41
             }

      
        43
        42
         

      
        44
        43
         

      ···
        71
        70
             = UserUpdatedInput Field String

      
        72
        71
             | UserChangedFormVariant Variant

      
        73
        72
             | UserClickedSubmit

      
        74
        
        -    | ApiSignInResponded (Result Http.Error Credentials)

      
        75
        
        -    | ApiSignUpResponded (Result Http.Error ())

      
        
        73
        +    | ApiSignInResponded (Result Api.Error Credentials)

      
        
        74
        +    | ApiSignUpResponded (Result Api.Error ())

      
        76
        75
         

      
        77
        76
         

      
        78
        77
         type Field

      ···
        198
        197
                 )

      
        199
        198
         

      
        200
        199
         

      
        201
        
        -viewError : Maybe Http.Error -> Html Msg

      
        
        200
        +viewError : Maybe Api.Error -> Html Msg

      
        202
        201
         viewError maybeError =

      
        203
        202
             case maybeError of

      
        204
        203
                 Just error ->

      
        205
        204
                     Html.div [ Attr.class "box bad" ]

      
        206
        205
                         [ Html.strong [ Attr.class "block titlebar" ] [ Html.text "Error" ]

      
        207
        
        -                , Html.text (Api.errorToFriendlyMessage error)

      
        
        206
        +                , Html.text (Api.errorMessage error)

      
        208
        207
                         ]

      
        209
        208
         

      
        210
        209
                 Nothing ->

      ···
        230
        229
         viewForgotPassword =

      
        231
        230
             Html.div []

      
        232
        231
                 [ Html.a

      
        233
        
        -            [ Attr.href "/forgot-password"

      
        234
        
        -            , Attr.class "gray"

      
        235
        
        -            ]

      
        
        232
        +            [ Attr.href "/forgot-password" ]

      
        236
        233
                     [ Html.text "Forgot password?" ]

      
        237
        234
                 ]

      
        238
        235
         

      
M web/src/Pages/Profile/Me.elm
···
        6
        6
         import Data.Me exposing (Me)

      
        7
        7
         import Effect exposing (Effect)

      
        8
        8
         import Html exposing (Html)

      
        9
        
        -import Http

      
        10
        9
         import Layouts

      
        11
        10
         import Page exposing (Page)

      
        12
        11
         import Route exposing (Route)

      ···
        45
        44
         

      
        46
        45
         

      
        47
        46
         type Msg

      
        48
        
        -    = ApiMeResponded (Result Http.Error Me)

      
        
        47
        +    = ApiMeResponded (Result Api.Error Me)

      
        49
        48
         

      
        50
        49
         

      
        51
        50
         update : Msg -> Model -> ( Model, Effect Msg )

      ···
        88
        87
                     viewUserDetails shared user

      
        89
        88
         

      
        90
        89
                 Api.Failure err ->

      
        91
        
        -            Html.text (Api.errorToFriendlyMessage err)

      
        
        90
        +            Html.text (Api.errorMessage err)

      
        92
        91
         

      
        93
        92
         

      
        94
        93
         viewUserDetails : Shared.Model -> Me -> Html Msg

      
M web/src/Shared/Msg.elm
···
        1
        1
         module Shared.Msg exposing (Msg(..))

      
        2
        2
         

      
        
        3
        +import Api

      
        3
        4
         import Data.Credentials exposing (Credentials)

      
        4
        
        -import Http

      
        5
        5
         import Time

      
        6
        6
         

      
        7
        7
         

      ···
        13
        13
               -- Session

      
        14
        14
             | CheckTokenExpiration Time.Posix

      
        15
        15
             | TriggerTokenRefresh

      
        16
        
        -    | ApiRefreshTokensResponded (Result Http.Error Credentials)

      
        
        16
        +    | ApiRefreshTokensResponded (Result Api.Error Credentials)

      
M web/tests/UnitTests/Data/Credentiala.elm
···
        9
        9
         suite : Test

      
        10
        10
         suite =

      
        11
        11
             describe "Data.Credentials"

      
        12
        
        -        [ test "decode credentials" <|

      
        
        12
        +        [ test "decode" <|

      
        13
        13
                     \_ ->

      
        14
        14
                         """

      
        15
        15
                         {

      
A web/tests/UnitTests/Data/Error.elm
···
        
        1
        +module UnitTests.Data.Error exposing (suite)

      
        
        2
        +

      
        
        3
        +import Data.Error

      
        
        4
        +import Expect

      
        
        5
        +import Json.Decode as Json

      
        
        6
        +import Test exposing (Test, describe, test)

      
        
        7
        +

      
        
        8
        +

      
        
        9
        +suite : Test

      
        
        10
        +suite =

      
        
        11
        +    describe "Data.Error"

      
        
        12
        +        [ test "decode" <|

      
        
        13
        +            \_ ->

      
        
        14
        +                """

      
        
        15
        +                {

      
        
        16
        +                    "message": "some kind of an error"

      
        
        17
        +                }

      
        
        18
        +                """

      
        
        19
        +                    |> Json.decodeString Data.Error.decode

      
        
        20
        +                    |> Expect.equal (Ok { message = "some kind of an error" })

      
        
        21
        +        ]

      
M web/tests/UnitTests/Data/Me.elm
···
        9
        9
         suite : Test

      
        10
        10
         suite =

      
        11
        11
             describe "Data.Me"

      
        12
        
        -        [ test "decode credentials" <|

      
        
        12
        +        [ test "decode" <|

      
        13
        13
                     \_ ->

      
        14
        14
                         """

      
        15
        15
                         {