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
| Class | Module | Meaning |
|---|---|---|
:invalid | Ltix.Errors.Invalid | Bad input — malformed JWT, missing claims, unknown registration |
:security | Ltix.Errors.Security | Security violation — bad signature, expired token, nonce replay |
:unknown | Ltix.Errors.Unknown | Unexpected 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
endMatching 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))
endError 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:
| Error | Fields | When |
|---|---|---|
MissingParameter | parameter, spec_ref | OIDC login request missing a required param |
MissingClaim | claim, spec_ref | JWT missing a required claim |
InvalidClaim | claim, value, spec_ref | JWT claim has an invalid value |
InvalidJson | spec_ref | JWT body isn't valid JSON |
RegistrationNotFound | issuer, client_id | No registration matches the issuer |
DeploymentNotFound | deployment_id | No deployment matches the JWT's deployment_id |
Security errors
These indicate the launch failed a security check:
| Error | Fields | When |
|---|---|---|
SignatureInvalid | spec_ref | JWT signature doesn't verify against the platform's public key |
TokenExpired | spec_ref | JWT exp claim is in the past |
IssuerMismatch | expected, actual, spec_ref | JWT iss doesn't match the registration |
AudienceMismatch | expected, actual, spec_ref | Registration's client_id not in JWT aud |
AlgorithmNotAllowed | algorithm, spec_ref | JWT uses something other than RS256 |
StateMismatch | spec_ref | OIDC state from callback doesn't match the session |
NonceMissing | spec_ref | JWT has no nonce claim |
NonceNotFound | spec_ref | Nonce wasn't issued by this tool |
NonceReused | spec_ref | Nonce was already consumed (replay attempt) |
KidMissing | spec_ref | JWT header has no kid field |
KidNotFound | kid, spec_ref | kid not found in the platform's JWKS |
AuthenticationFailed | error, error_description, spec_ref | Platform returned an error response |
Advantage service errors
These errors apply to LTI Advantage service calls (memberships, AGS, etc.) and OAuth authentication.
Invalid
| Error | Fields | When |
|---|---|---|
ServiceNotAvailable | service, spec_ref | Service not included in launch claims |
TokenRequestFailed | error, error_description, status, body, spec_ref | OAuth token request failed |
MalformedResponse | service, reason, spec_ref | Platform returned an unparseable response |
RosterTooLarge | count, max, spec_ref | get_members/2 exceeded max_members limit |
ScopeMismatch | scope, granted_scopes, spec_ref | Client lacks the required OAuth scope |
InvalidEndpoint | service, spec_ref | Wrong endpoint struct passed to a service |
Security
| Error | Fields | When |
|---|---|---|
AccessDenied | service, status, body, spec_ref | Platform returned 401/403 for a service request |
AccessTokenExpired | expires_at, spec_ref | OAuth 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