Skip to content

Improve handling of HttpResponseError objects being captured in Angular SDK #15239

Open

Description

@cobyeastwood183

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/angular

SDK Version

8.52.0

Framework Version

19.1.0

Link to Sentry event

No response

Reproduction Example/SDK Setup

https://github.com/cobyeastwood183/angular

error.json

{
    "headers": {
        "headers": {},
        "normalizedNames": {},
        "lazyUpdate": null
    },
    "status": 0,
    "statusText": "Unknown Error",
    "url": "http://some.example.com/test",
    "ok": false,
    "name": "HttpErrorResponse",
    "message": "Http failure response for http://some.example.com/test: 0 Unknown Error",
    "error": {
        "isTrusted": true
    }
}

Steps to Reproduce

  1. npm install
  2. add DSN
  3. npm start
  4. visit http://localhost:4200/test
  5. click button "Test API 2(Localhost)"

Expected Result

Expect a HttpException to be captured for HttpErrorResponse.

Actual Result

Instead 'unknown' is captured when caught by the ErrorHandler or 'Object.error' when caught by calling captureException.

Activity

  1. self-assigned this
    on Jan 31, 2025
  2. Lms24 commented on Jan 31, 2025

    @Lms24
    Member

    Dumping my thoughts from the slack conversation here. I don't have time to look at this today but will continue on Monday.

    I’ll summarize my preliminary findings from checking out the repro (thanks a lot btw!):

    • If you call captureException when handling the error, it won’t go through the ErrorHandler but will be sent directly to Sentry. In this case, the error object ends up with the Exception Captured With Keys message that you encountered.
      • Maybe we can apply the same extraction logic as in the ErrorHandler within the Angular SDK. let me think about this for a bit.
    • Letting the error bubble up via the ErrorHandler, leads to the error in Sentry.
      • The “” error type must have been changed recently in the product or during ingest because we add a title property to the event which is “” . The SDK doesn’t set title at all. So some kind of inference logic is going wrong here… I asked ingest the issues team about this
      • The other problem is that we capture much less information about this error than if we’d directly call captureException on it directly. Most importantly, we loose the stack trace
      • We could add a stack trace but due to Angular’s ZoneJS usage, it’s pretty worthless because it doesn’t point back to the user source code. Not sure yet if we can solve this but it doesn’t look trivial.
  3. self-assigned this
    on Mar 26, 2025
  4. 15 remaining items

  5. klyakh commented on Nov 29, 2025

    @klyakh

    Hi @Lms24 , I’m not ready to open a PR yet, because the code I have on my side feels more like a workaround tailored to my app than a clean, generic solution. But I can share what I found and how I’m currently handling it – maybe that helps.

    1. Main problem: different endpoints end up in the same Issue

    Right now, different HttpErrorResponse instances from different API endpoints are grouped into a single Issue. That makes it hard to use Sentry in a normal workflow: one Issue may contain many different root causes, some already fixed, some not.

    In my setup, this is what happens:

    1. The Angular integration extracts only a string (not an Error object) from HttpErrorResponse, something like:
      Http failure response for https://mydomain.com/api/asset/search?page=1&pageSize=50&keyword=test: 401 Unauthorized
    2. Grouping then happens by the stripped exception value. After stripping, this becomes something like:
      Http failure response for <url> <int> Unauthorized
    3. Because the URL is normalized away, errors from different endpoints are grouped together.

    So I lose the distinction between different API endpoints, which is often exactly what I want to group by.

    2. Why only a simple string is captured from HttpErrorResponse

    From what I see in extractHttpModuleError in packages/angular/src/errorhandler.ts, HttpErrorResponse.error is only used if it’s in one of these shapes:

    1. instanceof Error
    2. { name: string; message: string; stack?: string }
    3. instanceof ErrorEvent
    4. string

    If it’s none of those, it’s effectively ignored and the code falls back to HttpErrorResponse.message. That’s how we end up with a plain string like the example above.

    In my case, the server returns a structured JSON for server-side errors, e.g.:

    {
      "message": "An error has occurred.",
      "exceptionMessage": "This is test server error",
      "exceptionType": "System.NotImplementedException",
      "stackTrace": "<server-side stack trace here>"
    }

    This entire JSON ends up in HttpErrorResponse.error, but because it doesn’t match any of the four patterns, it’s ignored. As a result:

    • The meaningful fields from the server response are not visible in the Sentry event.
    • I also see <unknown> as the Issue title in the UI for these cases.

    3. My current workaround

    To work around this, I wrote a custom extractor that turns an HttpErrorResponse into a custom Error subclass, e.g. HttpErrorResponseError. That class contains:

    • endpoint URL
    • status code
    • the raw payload from HttpErrorResponse.error

    So instead of sending just the simple string, I send this custom error object to Sentry. I then:

    • add the structured server payload as part of the error (message/extra), and
    • tune the fingerprint logic to group events based on properties of this custom error (for example, using the HTTP method + path + status).

    This gives me usable grouping and much richer event data, but it feels quite hacky and app-specific.

    Happy to share more details if that helps.

  6. moved this from Waiting for: Community to Waiting for: Product Owner in GitHub Issues with 👀 3on Nov 29, 2025
  7. logaretm commented on Dec 1, 2025

    @logaretm
    Member

    @klyakh Thanks a lot of the detailed explanation, It would be great if you can share more details about the fingerprinting and enriched payload.

    I think it may not be as app-specific as you think. Grouping by URL seems reasonable to me as a default but if you can share details, I can discuss them with the team and see if you (or we) could PR this.

  8. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Dec 1, 2025
  9. Lms24 commented on Dec 1, 2025

    @Lms24
    Member

    Thank you for the great response! Just a couple of thoughts from my end:

    Grouping then happens by the stripped exception value. After stripping, this becomes something like:
    Http failure response for Unauthorized

    Could you link to such an event in Sentry? This makes it a bit easier for us to understand which grouping rules were applied.

    Grouping by URL seems reasonable to me as a default

    Agreed, as long as we don't have a client-side stack trace. I need to do some more investigative work to see if that's the case. My thinking is that there's likely a different stack trace/call stack for different endpoints, so ideally the errors shouldn't be grouped together in the first place. If we don't have a stack trace, falling back to the URL (and status code?) sounds reasonable to me on first glance. We could adjust the default fingerprint for HttpResponseErrors from within the SDK.

  10. MarcoGlauser commented on Jan 21, 2026

    @MarcoGlauser

    Thank you @klyakh for your comment! Armed with that knowledge, we asked Claude for a little proof of concept.

    import { HttpErrorResponse } from '@angular/common/http';
    
    /**
     * Custom error class for HTTP errors that provides better Sentry grouping.
     * Errors are grouped by URL path (without query params) and status code.
     *
     * Includes request metadata (method, body) if available from the interceptor.
     */
    export class HttpError extends Error {
        readonly status: number;
        readonly statusText: string;
        readonly url: string;
        constructor(httpErrorResponse: HttpErrorResponse) {
            // Extract the path from the URL (remove query params for better grouping)
            const urlPath = extractUrlPath(httpErrorResponse.url || 'unknown');
            // Create a descriptive message that includes status and URL
            const message = `HTTP ${httpErrorResponse.status} ${httpErrorResponse.statusText || 'Error'}: ${urlPath}`;
            super(message);
            this.name = 'HttpError';
            this.status = httpErrorResponse.status;
            this.statusText = httpErrorResponse.statusText;
            this.url = httpErrorResponse.url || 'unknown';
            this.responseBody = httpErrorResponse.error;
            if (httpErrorResponse['stack']) {
                this.stack = httpErrorResponse['stack'];
            }
        }
    }
    /**
     * Extracts the path portion from a URL, removing query parameters and fragments.
     * This helps with grouping similar errors together in Sentry.
     *
     * @example
     * extractUrlPath('https://api.example.com/users/123?foo=bar') // returns '/users/123'
     * extractUrlPath('https://api.example.com/trips/456/book') // returns '/trips/456/book'
     */
    export function extractUrlPath(url: string): string {
        try {
            const parsedUrl = new URL(url);
            return parsedUrl.pathname;
        } catch {
            // If URL parsing fails, try to extract path manually
            const pathMatch = url.match(/https?:\/\/[^/]+(\/[^?#]*)/);
            return pathMatch?.[1] || url;
        }
    }
    /**
     * Checks if an error is an Angular HttpErrorResponse.
     */
    function isHttpErrorResponse(error: unknown): error is HttpErrorResponse {
        return (
            error !== null &&
            typeof error === 'object' &&
            (error as HttpErrorResponse).name === 'HttpErrorResponse'
        );
    }
    /**
     * Custom error extractor for Sentry that properly handles Angular HttpErrorResponse.
     *
     * When an HttpErrorResponse is captured, this extractor converts it to a proper
     * Error object that Sentry can understand and group correctly.
     *
     * Errors are grouped by:
     * - URL path (without query parameters)
     * - HTTP status code
     *
     * @example
     * Sentry.createErrorHandler({
     *     extractor: httpErrorExtractor,
     * });
     */
    export function httpErrorExtractor(
        error: unknown,
        defaultExtractor: (error: unknown) => unknown
    ): unknown {
        // First, use the default extractor to unwrap any Angular error wrapping
        const extractedError = defaultExtractor(error);
        // Check if the extracted error is an HttpErrorResponse
        if (isHttpErrorResponse(extractedError)) {
            return new HttpError(extractedError);
        }
        // Also check the original error in case it wasn't properly extracted
        if (isHttpErrorResponse(error)) {
            return new HttpError(error);
        }
        // For all other errors, return the default extracted error
        return extractedError;
    }

    This works a lot better than the out of box experience, but grouping of events etc. is still not quite there. But it's a starting point.

    We're also experimenting with an HTTP Interceptor to annotate these errors with the request body that caused it.

  11. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jan 21, 2026
  12. chargome commented on Jan 22, 2026

    @chargome
    Member

    Could anyone actually running into this share a link to an event in Sentry?

  13. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jan 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions