Skip to content

Include Request ID and URL in Error Results #12

@MarketDataApp

Description

@MarketDataApp

Summary

Add request_id (from cf-ray header), request_url, timestamp, and status_code to HTTP-related exceptions and expose them via MarketDataClientErrorResult with support ticket helper methods to improve debugging and support ticket resolution.

Problem

When users report issues to support, they need to provide the cf-ray request ID for investigation. Currently:

  1. Users must parse log files to find the request ID
  2. If logging is disabled or logs are lost, the request ID is unrecoverable
  3. The request URL is not accessible, making it harder to identify which endpoint failed
  4. There's no easy way to generate a formatted support ticket with all necessary context

Proposed Solution

Scope

Only HTTP-related exceptions will carry request context. Validation exceptions that occur before a request is made cannot have this information.

Exception Has Request Context
RateLimitError Yes
BadStatusCodeError Yes
RequestError Yes
KeywordOnlyArgumentError No (validation)
InvalidStatusDataError No (parsing)
MinMaxDateValidationError No (validation)

Implementation

1. Update HTTP-Related Exception Classes

Add request_id, request_url, status_code, and timestamp to HTTP exceptions:

# exceptions.py
from datetime import datetime, timezone

class RateLimitError(Exception):
    """Raised when API rate limit is exceeded."""

    def __init__(
        self,
        message: str,
        status_code: int = 0,
        request_id: str | None = None,
        request_url: str | None = None,
    ):
        super().__init__(message)
        self.status_code = status_code
        self.request_id = request_id
        self.request_url = request_url
        self.timestamp = datetime.now(timezone.utc)


class BadStatusCodeError(Exception):
    """Raised when API returns an error status code."""

    def __init__(
        self,
        message: str,
        status_code: int = 0,
        request_id: str | None = None,
        request_url: str | None = None,
    ):
        super().__init__(message)
        self.status_code = status_code
        self.request_id = request_id
        self.request_url = request_url
        self.timestamp = datetime.now(timezone.utc)


class RequestError(Exception):
    """Raised when a request fails after retries."""

    def __init__(
        self,
        message: str,
        status_code: int = 0,
        request_id: str | None = None,
        request_url: str | None = None,
    ):
        super().__init__(message)
        self.status_code = status_code
        self.request_id = request_id
        self.request_url = request_url
        self.timestamp = datetime.now(timezone.utc)

2. Add Support Ticket Helpers to MarketDataClientErrorResult

# sdk_error.py
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

class MarketDataClientErrorResult:
    """Special result type for handling errors."""

    def __init__(self, error: Exception):
        self.error = error

    @property
    def request_id(self) -> str | None:
        """The cf-ray request ID, if available."""
        return getattr(self.error, "request_id", None)

    @property
    def request_url(self) -> str | None:
        """The request URL, if available."""
        return getattr(self.error, "request_url", None)

    @property
    def timestamp(self) -> datetime | None:
        """The timestamp when the exception occurred (UTC), if available."""
        return getattr(self.error, "timestamp", None)

    @property
    def status_code(self) -> int:
        """The HTTP status code, if available."""
        return getattr(self.error, "status_code", 0)

    def get_support_context(self) -> dict:
        """
        Get all support ticket context as a dictionary.

        Useful for structured logging (JSON, log aggregation systems)
        or when you need to process the error details programmatically.

        Example:
            if isinstance(result, MarketDataClientErrorResult):
                logger.error("API Error", extra=result.get_support_context())
        """
        timestamp_str = None
        if self.timestamp:
            eastern = self.timestamp.astimezone(ZoneInfo("America/New_York"))
            timestamp_str = eastern.isoformat()

        return {
            "timestamp": timestamp_str,
            "request_id": self.request_id,
            "url": self.request_url,
            "http_code": self.status_code,
            "message": str(self.error),
            "exception_type": self.error.__class__.__name__,
        }

    def get_support_info(self) -> str:
        """
        Get a pre-formatted string with all information needed for a support ticket.

        Copy and paste this output directly into your support request at
        support@marketdata.app or in the customer dashboard.

        Example:
            if isinstance(result, MarketDataClientErrorResult):
                print(result.get_support_info())
        """
        timestamp_str = "N/A"
        if self.timestamp:
            eastern = self.timestamp.astimezone(ZoneInfo("America/New_York"))
            timestamp_str = eastern.strftime("%Y-%m-%d %H:%M:%S %Z")

        lines = [
            "--- MARKET DATA SUPPORT INFO ---",
            f"Timestamp:    {timestamp_str}",
            f"Request ID:   {self.request_id or 'N/A'}",
            f"URL:          {self.request_url or 'N/A'}",
            f"HTTP Code:    {self.status_code}",
            f"Error:        {self.error}",
            "--------------------------------",
        ]
        return "\n".join(lines)

    def __repr__(self) -> str:
        parts = [
            f"MarketDataClientErrorResult(",
            f"error={self.error.__class__.__name__}",
            f", message={str(self.error)!r}",
        ]
        if self.request_id:
            parts.append(f", request_id={self.request_id!r}")
        if self.request_url:
            parts.append(f", request_url={self.request_url!r}")
        if self.timestamp:
            parts.append(f", timestamp={self.timestamp.isoformat()!r}")
        parts.append(")")
        return "".join(parts)

    def __str__(self) -> str:
        return self.__repr__()

3. Update Client to Pass Context When Raising Exceptions

Update _validate_response_status_code in client.py to pass all context:

def _validate_response_status_code(
    self,
    response: Response,
    retry_status_codes: list[int] | int | Callable,
    raise_for_status: bool,
) -> None:
    request_id = response.headers.get("cf-ray")
    request_url = str(response.request.url)
    status_code = response.status_code

    # Pass status_code, request_id, request_url to all exceptions...

Usage After Implementation

Quick Support Ticket Generation

result = client.stocks.quotes("INVALID_SYMBOL")

if isinstance(result, MarketDataClientErrorResult):
    # Formatted block ready to paste into a support ticket
    print(result.get_support_info())

Output:

--- MARKET DATA SUPPORT INFO ---
Timestamp:    2026-01-25 10:30:45 EST
Request ID:   9c340f7d6be275f3-EZE
URL:          https://api.marketdata.app/v1/stocks/quotes/INVALID_SYMBOL/?format=json
HTTP Code:    400
Error:        Bad parameters, please check API documentation.
--------------------------------

Structured Logging

if isinstance(result, MarketDataClientErrorResult):
    context = result.get_support_context()
    logger.error("API request failed", extra=context)

    # Or encode as JSON
    print(json.dumps(context, indent=2))

Output:

{
    "timestamp": "2026-01-25T10:30:45-05:00",
    "request_id": "9c340f7d6be275f3-EZE",
    "url": "https://api.marketdata.app/v1/stocks/quotes/INVALID_SYMBOL/?format=json",
    "http_code": 400,
    "message": "Bad parameters, please check API documentation.",
    "exception_type": "BadStatusCodeError"
}

Accessing Individual Properties

if isinstance(result, MarketDataClientErrorResult):
    print(f"Error: {result.error}")
    print(f"Request ID: {result.request_id}")
    print(f"URL: {result.request_url}")
    print(f"HTTP Code: {result.status_code}")
    print(f"Timestamp: {result.timestamp}")

Benefits

  1. Faster support resolution - Users can immediately provide request_id in tickets
  2. Better debugging - URL shows exactly which endpoint failed
  3. No log parsing required - Context is programmatically accessible
  4. Graceful degradation - Properties return None when context isn't available
  5. Consistent with PHP SDK - Implements same pattern as MarketDataException helper methods
  6. Support-ready output - get_support_info() provides copy-paste ready text
  7. Structured logging support - get_support_context() returns dict for log aggregation

Backward Compatibility

  • Exception constructors accept new optional keyword arguments with None defaults
  • Existing except RateLimitError handlers continue to work
  • MarketDataClientErrorResult maintains same interface, adds read-only properties
  • No changes to public method signatures

Files to Modify

File Changes
src/marketdata/exceptions.py Add request_id, request_url, status_code, timestamp to HTTP exceptions
src/marketdata/sdk_error.py Add properties and get_support_info(), get_support_context() methods
src/marketdata/client.py Pass context when raising exceptions
src/tests/test_exceptions.py Add tests for new attributes
src/tests/test_client.py Verify context is passed correctly

Related

  • PHP SDK: MarketDataException with getRequestId(), getRequestUrl(), getTimestamp(), getSupportInfo(), getSupportContext()
  • C# SDK: MarketDataException.RequestId and MarketDataException.RequestUrl

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions