Understanding Pike Error Responses

View Source

This guide explains the common error responses you might encounter when using Pike, why they occur, and how to debug and resolve them.

Error Response Types

Pike's authorization system produces several types of error responses through its responder interface. Understanding these errors will help you debug authentication and authorization issues in your application.

Common Error Types

Error CodeHTTP StatusDescription
:missing_key401 UnauthorizedNo Bearer token was provided in the request
:invalid_format400 Bad RequestThe provided token format is invalid
:not_found403 ForbiddenThe provided token doesn't exist in the store
:disabled403 ForbiddenThe token exists but has been disabled
:unauthorized_resource403 ForbiddenThe token lacks permission for the requested resource
:unauthorized_action403 ForbiddenThe token lacks permission for the specific action

| :store_error | 500 Internal Server Error | An unexpected error occurred in the store

Debugging Error Responses

Missing Key (401 Unauthorized)

Why This Happens

A :missing_key error occurs when:

  • The Authorization header is completely absent from the request
  • The Authorization header is present but doesn't use the Bearer scheme
  • The Authorization header uses Bearer scheme but doesn't include a token

Example Response

Missing API key

How to Debug

  1. Check if your client is including the Authorization header
  2. Verify the header format is correct: Authorization: Bearer your-api-key
  3. Ensure there are no extra spaces or special characters in the header
# Inspecting headers in a controller for debugging
def debug_action(conn, _params) do
  IO.inspect(conn.req_headers, label: "Request Headers")
  # Rest of your action...
end

Not Found (403 Forbidden) / Invalid Format (400 Bad Request)

Why This Happens

A :not_found error occurs when:

  • The token doesn't exist in the configured store
  • The token has been revoked or deleted
  • The token has expired (if using a store with expiration)
  • The token format is invalid or corrupted

Example Response

API key not found

How to Debug

  1. Verify the token exists in your store:
# In an IEx console
iex> Pike.get_key("your-api-key")
:error # Token doesn't exist

# Or if using a custom store
iex> MyApp.CustomStore.get_key("your-api-key")
:error # Token doesn't exist
  1. Check your store implementation for issues:

    • Database connectivity problems
    • Cache inconsistencies
    • Serialization/deserialization errors
  2. Enable detailed logging for your store:

defmodule MyApp.DebugStore do
  @behaviour Pike.Store
  
  require Logger
  
  @impl true
  def get_key(key) do
    Logger.debug("Fetching key: #{key}")
    
    result = MyApp.RealStore.get_key(key)
    
    case result do
      {:ok, key_data} -> 
        Logger.debug("Found key data: #{inspect(key_data)}")
        result
      :error -> 
        Logger.debug("Key not found: #{key}")
        result
    end
  end
  
  # Implement other callbacks...
end

Unauthorized Action (403 Forbidden)

Why This Happens

An :unauthorized_action error occurs when:

  • The token exists and is valid, but doesn't have the required permissions
  • The token's permissions don't include the resource being accessed
  • The token's permissions include the resource but not the specific action
  • A custom authorization rule rejected the request

Example Response

Unauthorized action

How to Debug

  1. Check the key's permissions:
# In an IEx console
iex> {:ok, key_data} = Pike.get_key("your-api-key")
iex> key_data.permissions
[
  %{resource: "Products", scopes: [:read]},
  %{resource: "Orders", scopes: [:read, :write]}
]
  1. Verify the required permissions for the action:
# In your controller
@require_permission "Products", :write  # This requires :write permission on Products
def update(conn, _params) do
  # ...
end
  1. Test the permission check directly:
iex> {:ok, key_data} = Pike.get_key("your-api-key")
iex> Pike.action?(key_data, "Products", :write)
false  # This indicates lack of permission
  1. Add debug logging to controller actions:
defmodule MyAppWeb.ProductController do
  use MyAppWeb, :controller
  use Pike.Authorization
  
  require Logger
  
  @require_permission "Products", :write
  def update(conn, params) do
    api_key = conn.assigns[:pike_api_key]
    Logger.debug("API Key: #{inspect(api_key)}")
    Logger.debug("Required permission: Products:write")
    
    # Rest of your action...
  end
end

Store Error (500 Internal Server Error)

Why This Happens

A :store_error occurs when:

  • The store implementation raises an exception
  • Database connectivity issues
  • Memory corruption in ETS-based stores
  • Programming errors in custom store implementations

Example Response

Internal authorization error

How to Debug

  1. Check your application logs for error messages and stack traces
  2. Add exception handling to your store implementation:
defmodule MyApp.SafeStore do
  @behaviour Pike.Store
  
  require Logger
  
  @impl true
  def get_key(key) do
    try do
      MyApp.RealStore.get_key(key)
    rescue
      e ->
        Logger.error("Error in get_key: #{inspect(e)}")
        Logger.error("Stacktrace: #{Exception.format_stacktrace(__STACKTRACE__)}")
        :error
    end
  end
  
  # Implement other callbacks with similar exception handling...
end

Custom Error Handling

You can customize how Pike responds to errors by implementing the Pike.Responder behavior:

defmodule MyApp.DetailedResponder do
  @behaviour Pike.Responder
  
  import Plug.Conn
  
  @impl true
  def auth_failed(conn, :missing_key) do
    conn
    |> put_status(:unauthorized)
    |> Phoenix.Controller.json(%{
        error: "Authentication Required",
        details: "Please provide an API key via the Authorization header: Authorization: Bearer your-api-key",
        docs_url: "https://docs.example.com/api/authentication"
    })
    |> halt()
  end
  
  @impl true
  def auth_failed(conn, :not_found) do
    conn
    |> put_status(:unauthorized)
    |> Phoenix.Controller.json(%{
        error: "Invalid API Key",
        details: "The provided API key was not found or is no longer valid",
        docs_url: "https://docs.example.com/api/authentication#valid-keys"
    })
    |> halt()
  end
  
  @impl true
  def auth_failed(conn, :unauthorized_action) do
    # Extract current user info for better error context
    api_key = conn.assigns[:pike_api_key]
    
    # Get the current controller and action
    controller = Phoenix.Controller.controller_module(conn)
    action = Phoenix.Controller.action_name(conn)
    
    # Generate a helpful error message
    conn
    |> put_status(:forbidden)
    |> Phoenix.Controller.json(%{
        error: "Insufficient Permissions",
        details: "Your API key doesn't have the required permissions for this operation",
        key_id: api_key.key,
        resource: controller |> Module.split() |> List.last() |> String.replace("Controller", ""),
        action: action,
        current_permissions: api_key.permissions,
        docs_url: "https://docs.example.com/api/permissions"
    })
    |> halt()
  end
  
  @impl true
  def auth_failed(conn, :store_error) do
    request_id = Logger.metadata()[:request_id] || "unknown"
    
    conn
    |> put_status(:internal_server_error)
    |> Phoenix.Controller.json(%{
        error: "Server Error",
        details: "An unexpected error occurred during authorization",
        request_id: request_id,
        support_contact: "api-support@example.com"
    })
    |> halt()
  end
end

Then configure Pike to use your custom responder:

# In your router or application config
plug Pike.AuthorizationPlug, on_auth_failure: {MyApp.DetailedResponder, :auth_failed}

Logging and Monitoring

For effective debugging in production, implement comprehensive logging:

Request Logging Middleware

defmodule MyApp.ApiRequestLogger do
  @behaviour Plug
  
  require Logger
  import Plug.Conn
  
  def init(opts), do: opts
  
  def call(conn, _opts) do
    start = System.monotonic_time()
    
    # Add request_id to the connection
    request_id = Logger.metadata()[:request_id] || generate_request_id()
    Logger.metadata(request_id: request_id)
    
    conn = put_private(conn, :api_request_start, start)
    
    # Log the request
    Logger.info("API Request: #{conn.method} #{conn.request_path}",
      request_id: request_id,
      remote_ip: format_ip(conn.remote_ip),
      headers: redact_headers(conn.req_headers)
    )
    
    # Register a callback to log after the response
    register_before_send(conn, fn conn ->
      duration = System.monotonic_time() - conn.private[:api_request_start]
      duration_ms = System.convert_time_unit(duration, :native, :millisecond)
      
      log_level = if conn.status >= 400, do: :warn, else: :info
      
      Logger.log(log_level, "API Response: #{conn.status}",
        request_id: request_id,
        duration_ms: duration_ms,
        method: conn.method,
        path: conn.request_path,
        status: conn.status,
        api_key: conn.assigns[:pike_api_key] && conn.assigns[:pike_api_key].key
      )
      
      conn
    end)
  end
  
  defp generate_request_id, do: Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
  
  defp format_ip(ip), do: ip |> Tuple.to_list() |> Enum.join(".")
  
  defp redact_headers(headers) do
    Enum.map(headers, fn
      {"authorization", _} -> {"authorization", "[REDACTED]"}
      other -> other
    end)
  end
end

Add this plug to your API pipeline:

pipeline :api do
  plug :accepts, ["json"]
  plug MyApp.ApiRequestLogger
  plug Pike.AuthorizationPlug
end

Common Issues and Solutions

Inconsistent Authorization Results

Symptoms:

  • Authorization works sometimes but fails other times
  • Different nodes handle the same request differently

Possible Causes:

  • Cache inconsistency in distributed environments
  • Race conditions in store implementation
  • Stale data in caches

Solutions:

  • Implement proper cache invalidation
  • Add versioning to your keys
  • Use distributed ETS or a shared cache (Redis)
  • Add logging to track key state changes

Unexpected 401 Errors

Symptoms:

  • API keys that should be valid return 401 Unauthorized
  • Keys work in testing but fail in production

Possible Causes:

  • Different store implementations between environments
  • Key serialization/deserialization issues
  • Case sensitivity in key handling
  • Whitespace or encoding issues in tokens

Solutions:

  • Standardize store implementations across environments
  • Normalize keys (trim whitespace, handle case consistently)
  • Add detailed logging for key lookup failures
  • Create a test endpoint that returns key details

Performance Degradation

Symptoms:

  • Authorization checks become slow over time
  • High latency for API requests

Possible Causes:

  • Inefficient store implementation
  • Database connection pool exhaustion
  • Memory leaks in custom stores
  • Growing number of keys affecting lookup performance

Solutions:

  • Optimize store implementation
  • Add performance metrics and monitoring
  • Implement caching for frequent lookups
  • Consider indexing strategies for database-backed stores

Debugging Checklist

When troubleshooting Pike authorization issues:

  1. Verify the request:

    • Check that the Authorization header is present and correctly formatted
    • Ensure there are no encoding issues with the token
  2. Validate the token:

    • Confirm the token exists in your store
    • Check that the token hasn't expired or been revoked
  3. Check permissions:

    • Verify the token has the necessary permissions
    • Ensure the controller is requiring the correct permissions
    • Check for typos in resource names or action atoms
  4. Inspect the environment:

    • Confirm the correct store is being used
    • Check for environment-specific configuration issues
    • Verify database connectivity for database-backed stores
  5. Review logs:

    • Look for error messages or exceptions
    • Check for patterns in failed requests
    • Analyze timing data for performance bottlenecks

Conclusion

Effective debugging of Pike's error responses requires understanding the authorization flow and implementing proper logging and monitoring. By customizing error responses and following the debugging techniques in this guide, you can quickly identify and resolve authorization issues in your application.

Remember that security-related errors should provide enough information for legitimate users to troubleshoot their issues, but not so much that they expose sensitive information or assist potential attackers.