When a launch fails, you need to know what went wrong and how to respond. Ltix returns {:error, exception} from both handle_login/3 and handle_callback/3, with errors organized into three classes using Splode. You can match on the category without knowing every individual error type.

Error classes

ClassModuleMeaning
:invalidLtix.Errors.InvalidBad input — malformed JWT, missing claims, unknown registration
:securityLtix.Errors.SecuritySecurity violation — bad signature, expired token, nonce replay
:unknownLtix.Errors.UnknownUnexpected failure — network errors, bugs

Matching on class

Every Ltix error struct has a class field you can match on:

case Ltix.handle_callback(params, state) do
  {:ok, context} ->
    handle_launch(conn, context)

  {:error, error} ->
    case error.class do
      :invalid ->
        conn |> put_status(400) |> text("Bad request: #{Exception.message(error)}")

      :security ->
        conn |> put_status(401) |> text("Unauthorized: #{Exception.message(error)}")

      :unknown ->
        Logger.error("LTI launch failed: #{Exception.message(error)}")
        conn |> put_status(500) |> text("Internal error")
    end
end

Matching on specific errors

For finer control, match on the error struct directly:

case Ltix.handle_login(params, launch_url) do
  {:ok, result} ->
    # ...

  {:error, %Ltix.Errors.Invalid.RegistrationNotFound{issuer: issuer}} ->
    Logger.warning("Unknown platform attempted login: #{issuer}")
    conn |> put_status(404) |> text("Platform not registered")

  {:error, %Ltix.Errors.Invalid.MissingParameter{parameter: param}} ->
    conn |> put_status(400) |> text("Missing required parameter: #{param}")

  {:error, error} ->
    conn |> put_status(400) |> text(Exception.message(error))
end

Error messages and spec references

Every error produces a human-readable message via Exception.message/1 and most carry a spec_ref field pointing to the violated spec section:

error = %Ltix.Errors.Security.TokenExpired{spec_ref: "Sec §5.1.3 step 5"}

Exception.message(error)
#=> "JWT token has expired [Sec §5.1.3 step 5]"

error.spec_ref
#=> "Sec §5.1.3 step 5"

The spec reference is useful for debugging — it tells you exactly which validation step failed.

Invalid errors

These indicate problems with the incoming data:

ErrorFieldsWhen
MissingParameterparameter, spec_refOIDC login request missing a required param
MissingClaimclaim, spec_refJWT missing a required claim
InvalidClaimclaim, value, spec_refJWT claim has an invalid value
InvalidJsonspec_refJWT body isn't valid JSON
RegistrationNotFoundissuer, client_idNo registration matches the issuer
DeploymentNotFounddeployment_idNo deployment matches the JWT's deployment_id

Security errors

These indicate the launch failed a security check:

ErrorFieldsWhen
SignatureInvalidspec_refJWT signature doesn't verify against the platform's public key
TokenExpiredspec_refJWT exp claim is in the past
IssuerMismatchexpected, actual, spec_refJWT iss doesn't match the registration
AudienceMismatchexpected, actual, spec_refRegistration's client_id not in JWT aud
AlgorithmNotAllowedalgorithm, spec_refJWT uses something other than RS256
StateMismatchspec_refOIDC state from callback doesn't match the session
NonceMissingspec_refJWT has no nonce claim
NonceNotFoundspec_refNonce wasn't issued by this tool
NonceReusedspec_refNonce was already consumed (replay attempt)
KidMissingspec_refJWT header has no kid field
KidNotFoundkid, spec_refkid not found in the platform's JWKS
AuthenticationFailederror, error_description, spec_refPlatform returned an error response

Advantage service errors

These errors apply to LTI Advantage service calls (memberships, AGS, etc.) and OAuth authentication.

Invalid

ErrorFieldsWhen
ServiceNotAvailableservice, spec_refService not included in launch claims
TokenRequestFailederror, error_description, status, body, spec_refOAuth token request failed
MalformedResponseservice, reason, spec_refPlatform returned an unparseable response
RosterTooLargecount, max, spec_refget_members/2 exceeded max_members limit
ScopeMismatchscope, granted_scopes, spec_refClient lacks the required OAuth scope
InvalidEndpointservice, spec_refWrong endpoint struct passed to a service

Security

ErrorFieldsWhen
AccessDeniedservice, status, body, spec_refPlatform returned 401/403 for a service request
AccessTokenExpiredexpires_at, spec_refOAuth access token has expired

Logging recommendations

Invalid errors are usually the platform's fault (misconfiguration, bad requests). Security errors could be attacks or clock skew. Unknown errors need investigation:

{:error, error} ->
  case error.class do
    :invalid ->
      Logger.info("LTI invalid request: #{Exception.message(error)}")

    :security ->
      Logger.warning("LTI security violation: #{Exception.message(error)}")

    :unknown ->
      Logger.error("LTI unexpected error: #{Exception.message(error)}")
  end