【问题标题】:How to debug XHR occurring an Angular 10 application如何调试发生 Angular 10 应用程序的 XHR
【发布时间】:2022-01-14 21:27:17
【问题描述】:

默认情况下,Angular 10 使用 XHR 后端,但有可用的 JSONP 后端。 我从HttpClient 收到了一些奇怪的结果,这与使用其他工具与后端 API 交互时收到的数据不同。

我希望能够查看原始XMLHttpRequest 流量中发生的情况。我怀疑HttpInterceptor 正在做一些时髦的事情,当我添加我自己的HttpInterceptor 时,有效负载已经与我期望看到的有所不同。观看 XHR 将“证明”这一理论。

【问题讨论】:

    标签: xmlhttprequest angular-httpclient


    【解决方案1】:

    我添加一些日志记录的方法是将 XHR 后端从 https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts 复制到子类中。需要进行一些小改动,因为并非所有内容都从 '@angular/common/http' 导出。

    (注意以下是 MIT 许可的

    import { HttpXhrBackend, HttpHeaders, HttpRequest, HttpResponse, HttpBackend, HttpEvent } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    import { Observable, Observer } from 'rxjs';
    
    import {HttpDownloadProgressEvent, HttpErrorResponse, HttpEventType, HttpHeaderResponse, HttpUploadProgressEvent} from '@angular/common/http';
    
    const XSSI_PREFIX = /^\)\]\}',?\n/;
    
    /**
     * Determine an appropriate URL for the response, by checking either
     * XMLHttpRequest.responseURL or the X-Request-URL header.
     */
    function getResponseUrl(xhr: any): string|null {
      if ('responseURL' in xhr && xhr.responseURL) {
        return xhr.responseURL;
      }
      if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
        return xhr.getResponseHeader('X-Request-URL');
      }
      return null;
    }
    
    @Injectable()
    export class CustomHttpBackend extends HttpXhrBackend {
    
      handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        console.log(req);
    
        // Quick check to give a better error message when a user attempts to use
        // HttpClient.jsonp() without installing the HttpClientJsonpModule
        if (req.method === 'JSONP') {
        throw new Error(
            `Attempted to construct Jsonp request without HttpClientJsonpModule installed.`);
        }
    
        // Everything happens on Observable subscription.
        return new Observable((observer: Observer<HttpEvent<any>>) => {
        // Start by setting up the XHR object with request method, URL, and withCredentials flag.
        const xhr = new XMLHttpRequest();
        xhr.open(req.method, req.urlWithParams);
        if (!!req.withCredentials) {
            xhr.withCredentials = true;
        }
    
        // Add all the requested headers.
        Array.from(req.headers.keys())
            .forEach(key => xhr.setRequestHeader(key, req.headers.get(key)!));
    
        // Add an Accept header if one isn't present already.
        if (!req.headers.has('Accept')) {
            xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
        }
    
        // Auto-detect the Content-Type header if one isn't present already.
        if (!req.headers.has('Content-Type')) {
            const detectedType = req.detectContentTypeHeader();
            // Sometimes Content-Type detection fails.
            if (detectedType !== null) {
            xhr.setRequestHeader('Content-Type', detectedType);
            }
        }
    
        // Set the responseType if one was requested.
        if (req.responseType) {
            const responseType = req.responseType.toLowerCase();
    
            // JSON responses need to be processed as text. This is because if the server
            // returns an XSSI-prefixed JSON response, the browser will fail to parse it,
            // xhr.response will be null, and xhr.responseText cannot be accessed to
            // retrieve the prefixed JSON data in order to strip the prefix. Thus, all JSON
            // is parsed by first requesting text and then applying JSON.parse.
            xhr.responseType = ((responseType !== 'json') ? responseType : 'text') as any;
        }
    
        // Serialize the request body if one is present. If not, this will be set to null.
        const reqBody = req.serializeBody();
    
        // If progress events are enabled, response headers will be delivered
        // in two events - the HttpHeaderResponse event and the full HttpResponse
        // event. However, since response headers don't change in between these
        // two events, it doesn't make sense to parse them twice. So headerResponse
        // caches the data extracted from the response whenever it's first parsed,
        // to ensure parsing isn't duplicated.
        let headerResponse: HttpHeaderResponse|null = null;
    
        // partialFromXhr extracts the HttpHeaderResponse from the current XMLHttpRequest
        // state, and memoizes it into headerResponse.
        const partialFromXhr = (): HttpHeaderResponse => {
            if (headerResponse !== null) {
            return headerResponse;
            }
    
            // Read status and normalize an IE9 bug (https://bugs.jquery.com/ticket/1450).
            const status: number = xhr.status === 1223 ? 204 : xhr.status;
            const statusText = xhr.statusText || 'OK';
    
            // Parse headers from XMLHttpRequest - this step is lazy.
            const headers = new HttpHeaders(xhr.getAllResponseHeaders());
    
            // Read the response URL from the XMLHttpResponse instance and fall back on the
            // request URL.
            const url = getResponseUrl(xhr) || req.url;
    
            // Construct the HttpHeaderResponse and memoize it.
            headerResponse = new HttpHeaderResponse({headers, status, statusText, url});
            return headerResponse;
        };
    
        // Next, a few closures are defined for the various events which XMLHttpRequest can
        // emit. This allows them to be unregistered as event listeners later.
    
        // First up is the load event, which represents a response being fully available.
        const onLoad = () => {
            // Read response state from the memoized partial data.
            let {headers, status, statusText, url} = partialFromXhr();
    
            // The body will be read out if present.
            let body: any|null = null;
    
            if (status !== 204) {
              // Use XMLHttpRequest.response if set, responseText otherwise.
              body = (typeof xhr.response === 'undefined') ? xhr.responseText : xhr.response;
            }
            console.log(body);
    
            // Normalize another potential bug (this one comes from CORS).
            if (status === 0) {
              status = !!body ? 200 : 0;
            }
    
            // ok determines whether the response will be transmitted on the event or
            // error channel. Unsuccessful status codes (not 2xx) will always be errors,
            // but a successful status code can still result in an error if the user
            // asked for JSON data and the body cannot be parsed as such.
            let ok = status >= 200 && status < 300;
    
            // Check whether the body needs to be parsed as JSON (in many cases the browser
            // will have done that already).
            if (req.responseType === 'json' && typeof body === 'string') {
            // Save the original body, before attempting XSSI prefix stripping.
            const originalBody = body;
            body = body.replace(XSSI_PREFIX, '');
            try {
                // Attempt the parse. If it fails, a parse error should be delivered to the user.
                body = body !== '' ? JSON.parse(body) : null;
            } catch (error) {
                // Since the JSON.parse failed, it's reasonable to assume this might not have been a
                // JSON response. Restore the original body (including any XSSI prefix) to deliver
                // a better error response.
                body = originalBody;
    
                // If this was an error request to begin with, leave it as a string, it probably
                // just isn't JSON. Otherwise, deliver the parsing error to the user.
                if (ok) {
                // Even though the response status was 2xx, this is still an error.
                ok = false;
                // The parse error contains the text of the body that failed to parse.
                body = {error, text: body};// as HttpJsonParseError
                }
            }
            }
    
            if (ok) {
            // A successful response is delivered on the event stream.
            observer.next(new HttpResponse({
                body,
                headers,
                status,
                statusText,
                url: url || undefined,
            }));
            // The full body has been received and delivered, no further events
            // are possible. This request is complete.
            observer.complete();
            } else {
            // An unsuccessful request is delivered on the error channel.
            observer.error(new HttpErrorResponse({
                // The error in this case is the response body (error from the server).
                error: body,
                headers,
                status,
                statusText,
                url: url || undefined,
            }));
            }
        };
    
        // The onError callback is called when something goes wrong at the network level.
        // Connection timeout, DNS error, offline, etc. These are actual errors, and are
        // transmitted on the error channel.
        const onError = (error: ProgressEvent) => {
            const {url} = partialFromXhr();
            const res = new HttpErrorResponse({
            error,
            status: xhr.status || 0,
            statusText: xhr.statusText || 'Unknown Error',
            url: url || undefined,
            });
            observer.error(res);
        };
    
        // The sentHeaders flag tracks whether the HttpResponseHeaders event
        // has been sent on the stream. This is necessary to track if progress
        // is enabled since the event will be sent on only the first download
        // progress event.
        let sentHeaders = false;
    
        // The download progress event handler, which is only registered if
        // progress events are enabled.
        const onDownProgress = (event: ProgressEvent) => {
            // Send the HttpResponseHeaders event if it hasn't been sent already.
            if (!sentHeaders) {
            observer.next(partialFromXhr());
            sentHeaders = true;
            }
    
            // Start building the download progress event to deliver on the response
            // event stream.
            let progressEvent: HttpDownloadProgressEvent = {
            type: HttpEventType.DownloadProgress,
            loaded: event.loaded,
            };
    
            // Set the total number of bytes in the event if it's available.
            if (event.lengthComputable) {
            progressEvent.total = event.total;
            }
    
            // If the request was for text content and a partial response is
            // available on XMLHttpRequest, include it in the progress event
            // to allow for streaming reads.
            if (req.responseType === 'text' && !!xhr.responseText) {
            progressEvent.partialText = xhr.responseText;
            }
    
            // Finally, fire the event.
            observer.next(progressEvent);
        };
    
        // The upload progress event handler, which is only registered if
        // progress events are enabled.
        const onUpProgress = (event: ProgressEvent) => {
            // Upload progress events are simpler. Begin building the progress
            // event.
            let progress: HttpUploadProgressEvent = {
            type: HttpEventType.UploadProgress,
            loaded: event.loaded,
            };
    
            // If the total number of bytes being uploaded is available, include
            // it.
            if (event.lengthComputable) {
            progress.total = event.total;
            }
    
            // Send the event.
            observer.next(progress);
        };
    
        // By default, register for load and error events.
        xhr.addEventListener('load', onLoad);
        xhr.addEventListener('error', onError);
        xhr.addEventListener('timeout', onError);
        xhr.addEventListener('abort', onError);
    
        // Progress events are only enabled if requested.
        if (req.reportProgress) {
            // Download progress is always enabled if requested.
            xhr.addEventListener('progress', onDownProgress);
    
            // Upload progress depends on whether there is a body to upload.
            if (reqBody !== null && xhr.upload) {
            xhr.upload.addEventListener('progress', onUpProgress);
            }
        }
    
        // Fire the request, and notify the event stream that it was fired.
        xhr.send(reqBody!);
        observer.next({type: HttpEventType.Sent});
    
        // This is the return from the Observable function, which is the
        // request cancellation handler.
        return () => {
            // On a cancellation, remove all registered event listeners.
            xhr.removeEventListener('error', onError);
            xhr.removeEventListener('abort', onError);
            xhr.removeEventListener('load', onLoad);
            xhr.removeEventListener('timeout', onError);
            if (req.reportProgress) {
            xhr.removeEventListener('progress', onDownProgress);
            if (reqBody !== null && xhr.upload) {
                xhr.upload.removeEventListener('progress', onUpProgress);
            }
            }
    
            // Finally, abort the in-flight request.
            if (xhr.readyState !== xhr.DONE) {
            xhr.abort();
            }
        };
        });
      }
    }
    

    然后在 app.module.ts 中,添加自定义提供者。

    import { HttpBackend } from '@angular/common/http';
    import { CustomHttpBackend } from './path/to/custom-http-backend';
    
    providers: [
        ...,
        {
          provide: HttpBackend,
          useClass: CustomHttpBackend,
        },
      ],
    

    【讨论】:

      猜你喜欢
      • 2018-10-26
      • 1970-01-01
      • 2017-01-13
      • 1970-01-01
      • 1970-01-01
      • 2012-09-17
      • 1970-01-01
      • 1970-01-01
      • 2011-02-22
      相关资源
      最近更新 更多