HTTP.jl client layer

I’m trying to implement an auth layer for HTTP.jl

module ApiClient

import HTTP
import Dates
import JSON

Base.@kwdef struct TokenInfo
    access_token::String = ""
    expires_at::Dates.DateTime = Dates.now(Dates.UTC) + Dates.Second(0)
    token_type::String = "Bearer"
end

struct JWTConfig
    client_id::String
    client_secret::String
    token_url::String
    token_info::Ref{TokenInfo}
end

function getToken(jwt_config::JWTConfig)
    response = HTTP.post(
        jwt_config.token_url,
        ["Content-Type" => "application/json"],
        JSON.json(
            Dict(
                "grant_type" => "client_credentials",
                "client_id" => jwt_config.client_id,
                "client_secret" => jwt_config.client_secret,
            ),
        ),
    )
    response_data = JSON.parse(String(response.body))
    return TokenInfo(
        response_data["access_token"],
        Dates.now(Dates.UTC) + Dates.Second(response_data["expires_in"]),
        response_data["token_type"],
    )
end

function AuthLayer(handler)
    # return handler function
    return function (req; jwt_config::JWTConfig, kwargs...)
        # we add a custom header with stringified auth creds
        try
            # Try the original request
            HTTP.setheader(req, "Authorization" => "Bearer $(jwt_config.token_info[].access_token)")
            return handler(req; kwargs...)
        catch e
            # Check if it's a 401 error
            if e isa HTTP.StatusError && e.status == 401
                # Refresh the token and retry the request
                jwt_config.token_info[] = getToken(jwt_config)
                HTTP.setheader(req, "Authorization" => "Bearer $(jwt_config.token_info[].access_token)")
                try
                    return handler(req; kwargs...)
                catch
                    throw(e)
                end
            else
                # If it's not a 401 error, rethrow the exception
                throw(e)
            end
        end
    end
end

HTTP.@client [AuthLayer]

end

But somehow this does not work correctly. I can access the token and resource I want, but when I first access the token I get half the response (literary the JSON body is cut off). If I switch on debugging I see, that the HTTP response is weird with many fields doubled, e.g. two times Content-Length: 107 and Content-Length: 26699.

How can I call the token requresh url when a request fails? Is this not supported?

Sorry for a bit of a rushed response, but yes, it can be a little tricky when trying to retry requests like this. You can check out some of the machinery we use/do in the RetryRequest layer here: HTTP.jl/src/clientlayers/RetryRequest.jl at ef9a169c5471e14558409a649057d773c09451f6 · JuliaWeb/HTTP.jl · GitHub

function AuthLayer(handler)
    # return handler function
    return function (req; jwt_config::JWTConfig, kwargs...)
        try
            # Try the original request
            HTTP.setheader(req, "Authorization" => "Bearer $(jwt_config.token_info[].access_token)")
            return handler(req; kwargs...)
        catch e
            # Check if it's a 401 error
            if e isa HTTP.StatusError && e.status == 401
                HTTP.reset!(req.response) # reset the original request for retry
                # Refresh the token and retry the request
                jwt_config.token_info[] = getToken(jwt_config)
                HTTP.setheader(req, "Authorization" => "Bearer $(jwt_config.token_info[].access_token)")
                try
                    return handler(req; kwargs...)
                catch
                    throw(e)
                end
            else
                # If it's not a 401 error, rethrow the exception
                throw(e)
            end
        end
    end
end
1 Like