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)

HTTP status codes

Every error has an HTTP status code derived from its class.

ClassStatus
:invalid400
:security401
:unknown500

Use Ltix.Errors.status_code/1 to get it.

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

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

Plug.Exception

If your application depends on Plug, Ltix errors automatically implement the Plug.Exception protocol. Phoenix error views and Plug.Debugger use the correct status code without any extra work on your part.

Matching on class

Every Ltix error struct has a class field you can match on for different handling per category.

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

  {:error, error} ->
    status = Ltix.Errors.status_code(error)

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

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

      :unknown ->
        Logger.error("LTI launch failed: #{Exception.message(error)}")
        conn |> put_status(status) |> 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

Single and grouped errors

A single failure returns its error struct directly. When validation finds several problems at once, such as two malformed claims, they are grouped in their class struct and errors lists each one.

case Ltix.LaunchClaims.from_json(claims) do
  {:ok, claims} ->
    claims

  {:error, %Ltix.Errors.Invalid{errors: errors}} ->
    Enum.each(errors, &Logger.warning(Exception.message(&1)))

  {:error, error} ->
    Logger.warning(Exception.message(error))
end

The message of a grouped error lists the message of each error it contains.

Invalid options

Ltix raises Ltix.Errors.Invalid.InvalidOption when a function gets an unknown option, a missing required option, or an option with a value of the wrong type.

Ltix.JWK.generate(key_sise: 4096)
** (Ltix.Errors.Invalid.InvalidOption) Option :key_sise is unknown

Error messages and spec references

Every error produces a human-readable message via Exception.message/1 and most have a spec_ref field with the violated spec section.

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

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

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

The spec reference is useful for debugging because it identifies exactly which validation step failed.

Invalid errors

These indicate problems with the incoming data or the calling code.

ErrorFieldsWhen
MissingParameterparameter, spec_refOIDC login request missing a required param
MissingClaimclaim, spec_refA JWT or platform response, such as a memberships page, is missing a required claim, such as sub or resource_link.id
InvalidClaimclaim, value, message, spec_refA claim in a JWT or platform response has an invalid value, or a custom claim parser returned an error
InvalidFieldstruct, field, value, reason, spec_refA constructor such as Ltix.Registration.new/1, Ltix.GradeService.Score.new/1, or Ltix.DeepLinking.ContentItem.Link.new/1 got an unknown, missing, or invalid field, or a line item to create or update has an invalid field
InvalidOptionkey, value, reasonA function got an unknown, missing, or invalid option
InvalidJSONspec_refThe ID token is not a JWT whose header and payload are JSON objects
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, or its iat claim is older than :max_token_age
TokenNotYetValidclaim, spec_refJWT iat or nbf claim is in the future
IssuerMismatchexpected, actual, spec_refJWT iss doesn't match the registration
AudienceMismatchclaim, expected, actual, spec_refJWT aud is not the registration's client_id or a list containing only it, or azp is not the client_id
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
LocalNetworkAccessDeniedurl, address_spaceThe platform's key set is on a local or loopback address that :local_network_access doesn't allow
AuthenticationFailederror, error_description, error_uri, spec_refPlatform returned an error response

Unknown errors

These indicate an unexpected failure outside the incoming request.

ErrorFieldsWhen
StorageErrorcallback, result, reasonA storage adapter callback returned {:error, reason}, an unexpected value, or a deployment for a different deployment_id
TransportErrorstatus, body, headers, url, spec_refAn HTTP request to the platform failed
RedirectNotAllowedstatus, url, location, spec_refA platform endpoint returned an unsafe redirect
KeySetNotStartedkey_setRaised when a launch needs a key set that is not started in the supervision tree
UnknownerrorAny other unexpected failure

Deep linking errors

These errors come from Ltix.DeepLinking.build_response/3. Content item constructors such as Ltix.DeepLinking.ContentItem.Link.new/1 return InvalidField. Errors about one item in a response include its index in the list you passed.

ErrorFieldsWhen
InvalidContentItemindex, value, reason, spec_refA response item is not a content item or has no valid type, or the items are not a list
InvalidMessageTypemessage_type, spec_refThe launch is not a deep linking request
ContentItemTypeNotAcceptedindex, type, accept_types, spec_refAn item's type is not in accept_types
ContentItemsExceedLimitcount, spec_refSeveral items when accept_multiple is false
LineItemNotAcceptedindex, spec_refA resource link has a line item when accept_line_item is false
PresentationTargetNotAcceptedindex, target, accept_presentation_document_targets, spec_refAn item's iframe, window, or embed is not in accept_presentation_document_targets
MediaTypeNotAcceptedindex, media_type, accept_media_types, spec_refA file's media type does not match accept_media_types

Advantage service errors

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

Invalid

ErrorFieldsWhen
ServiceNotAvailableservice, reason, spec_refThe launch has no claim for the service, an endpoint or URL the operation needs is missing, the endpoint does not support the service version, or the registration has no token endpoint
TokenRequestFailederror, error_description, status, body, spec_refOAuth token request failed
MalformedResponseservice, reason, spec_refPlatform returned an unparseable response
CollectionTooLargeservice, option, count, max, spec_refAn eager fetch such as list_results/2 exceeded its limit option, such as max_results
ScopeMismatchscopes, granted_scopes, spec_refClient lacks every scope in scopes that would authorize the request
InvalidEndpointservice, spec_refWrong endpoint struct passed to a service
MissingLineItemIdline_item, spec_refA grade operation received a line item without an id

Security

ErrorFieldsWhen
AccessDeniedstatus, body, url, spec_refPlatform responded to a service request or key set fetch with HTTP 401 or 403
LocalNetworkAccessDeniedurl, address_spaceA platform request would connect to a local or loopback address that :local_network_access doesn't allow
AccessTokenExpiredexpires_at, spec_refOAuth access token has expired

Unknown

ErrorFieldsWhen
TransportErrorstatus, body, headers, url, spec_refLtix could not connect to the platform, or the endpoint URL is neither an absolute HTTPS URL nor an HTTP URL for a loopback or local host
RedirectNotAllowedstatus, url, location, spec_refThe platform redirected to a different origin or an invalid location
KeySetNotStartedkey_setRaised when a launch needs a key set that is not started in the supervision tree

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