Angular2 REST request HTTP status code 401 changes

2019-04-18 10:14发布

问题:

I'm developing an Angular2 application. It seems when my access-token expires the 401 HTTP Status code gets changed to a value of 0 in the Response object. I'm receiving 401 Unauthorized yet the ERROR Response object has a status of 0. This is preventing me from trapping a 401 error and attempting to refresh the token. What's causing the 401 HTTP status code to be changed into HTTP status code of 0?

Here's screenshot from Firefox's console:

Here's my code:

get(url: string, options?: RequestOptionsArgs): Observable<any> 
{
    //console.log('GET REQUEST...', url);

    return super.get(url, options)
        .catch((err: Response): any =>
        {
            console.log('************* ERROR Response', err);

            if (err.status === 400 || err.status === 422)
            {
                return Observable.throw(err);
            }
            //NOT AUTHENTICATED
            else if (err.status === 401)
            {                   
                this.authConfig.DeleteToken();
                return Observable.throw(err);
            }               
            else
            {
                // this.errorService.notifyError(err);
                // return Observable.empty();
                return Observable.throw(err);
            }
        })
        // .retryWhen(error => error.delay(500))
        // .timeout(2000, new Error('delay exceeded'))
        .finally(() =>
        {
            //console.log('After the request...');
        });
}

This code resides in a custom http service that extends Angular2's HTTP so I can intercept errors in a single location.

In Google Chrome, I get this error message:

XMLHttpRequest cannot load https://api.cloudcms.com/repositories/XXXXXXXXXXXXXXXX/branches/XXXXXXXXXXXXXXXX/nodesXXXXXXXXXXXXXXXX. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://screwtopmedia.local.solutiaconsulting.com' is therefore not allowed access. The response had HTTP status code 401.

This is confusing because I am including 'Access-Control-Allow-Origin' header in request.

Here's a picture of results received in Google Chrome:

I've tried accessing 'WWW-Authenticate' Response Header as a means to trap for 401. However, the following code returns a NULL:

err.headers.get("WWW-Authenticate")

It's puzzling that I'm getting a CORS issue because I'm not getting any CORS errors when a valid access token is provided.

How do I trap for 401 HTTP status code? Why is 401 HTTP status code being changed to 0?

Thanks for your help.

回答1:

I work for Cloud CMS. I can confirm that we properly set CORS headers on API calls. However, for 401 responses the headers are not getting set properly. This has been resolved and the fix will be available on the public API at the end of this week.

BTW if you use our javascript driver https://github.com/gitana/gitana-javascript-driver instead of writing to the API directly then the re-auth flow is handled automatically and you do not need to trap 401 errors at all.



回答2:

The issue is related to CORS requests, see this github issue

No 'Access-Control-Allow-Origin' header is present on the requested resource

means that 'Access-Control-Allow-Origin' is required in the response headers.

Angular is not getting any status codes, that's why it gives you a 0 which is caused by browser not allowing the xml parser to parse the response due to invalid headers.

You need to append correct CORS headers to your error response as well as success.



回答3:

If cloudcms.com do to not set the Access-Control-Allow-Origin in 401 response, then nothing much you can do. You properly have to open support ticket with them to confirm if that is normal behavior.

javascript in browsers(FF, Chrome & Safari) I tested won't receive any info if CORS error occur, other than status 0. Angular2 has no control of it.


I created a simple test and get the same result as yours:

geturl() {
    console.log('geturl() clicked');
    let headers = new Headers({ 'Content-Type': 'application/xhtml+xml' });
    let options = new RequestOptions({ 'headers': headers });
    this.http.get(this.url)
        .catch((error): any => {
            console.log('************* ERROR Response', error);
            let errMsg = (error.message) ? error.message :
                error.status ? `${error.status} - ${error.statusText}` : 'Web Server error';
            return Observable.throw(error);
        })
        .subscribe(
        (i: Response) => { console.log(i); },
        (e: any) => { console.log(e); }
        );
}

After much googling, I found the following(https://github.com/angular/angular.js/issues/3336):

lucassp commented on Aug 19, 2013

I found the issue for a while now but I forgot post here a reply. EVERY response, even Error 500, must have the CORS headers attached. If the server doesn't attach the CORS headers to the Error 500 response then the XHR Object won't parse it, thus the XHR Object won't have any response body, status, or any other response data inside.

Result from Firefox network monitor seems supporting the above reason.

Javascript request will receive empty response.

Plain url request(copy&paste the link in the address bar) will get response body

Javascript use XHRHttpRequest for http communication.

When a XHR reply arrive, the browser will process the header, that's why you will see those 401 network messages. However, if the XHR reply is from a different host then the javascript AND the response header contain no CORS header(eg Access-Control-Allow-Origin: *), the browser will not pass any info back to the XHR layer. As a result, the reply body will be completely empty with no status(0).

I tested with FF 48.0.1, Chrome 52.0.2743.116 and Safari 9.1.2, and they all have the same behavior.

Use browser network monitor to check response header for those 401 Unauthorized entries, it is very likely that there is no Access-Control-Allow-Origin header and causing the issue you are facing. This can be a bug or by design on the service provider side.

Access-Control-Allow-Origin is a response only http header. For more information of https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#The_HTTP_response_headers



回答4:

According to W3C, if the status attribute is set to 0, it means that:

  • The state is UNSENT or OPENED.

or

  • The error flag is set.

I think that this isn't an Angular2 issue, it seems like your request has a problem.



回答5:

You should use a 3rd party http protocol monitor (like CharlesProxy) rather than Chrome dev tools to confirm which headers are actually being sent to the API service and if it is returning a 401.



回答6:

I had the same issue and was due to the server not sending the correct response (even though the console log stated a 401 the Angular error had status 0). My server was Tomcat with Java application using Spring MVC and Spring Security.

It is now working using folowing setup:

SecurityConfig.java

...
protected void configure(HttpSecurity http) throws Exception {
....
    http.exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler)
            .authenticationEntryPoint(authenticationEntryPoint);
    // allow CORS option calls
    http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
 ...

SomeAuthenticationEntryPoint.java

@Component
public class SomeAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    HttpStatus responseStatus = HttpStatus.UNAUTHORIZED;
    response.sendError(responseStatus.value(), responseStatus.getReasonPhrase());
    }
}

web.xml

<error-page>
    <error-code>403</error-code>
    <location>/errors/unauthorised</location>
</error-page>
<error-page>
    <error-code>401</error-code>
    <location>/errors/unauthorised</location>
</error-page>

Controller to handle the /errors/... previously defined

@RestController
@RequestMapping("/errors")
public class SecurityExceptionController {

    @RequestMapping("forbidden")
    public void resourceNotFound() throws Exception {
        // Intentionally empty
    }

    @RequestMapping("unauthorised")
    public void unAuthorised() throws Exception {
        // Intentionally empty
        }

    }
}