Not sure if this belongs in Boot or Framework or somewhere else..

Spring Boot version: 3.0.7

If my Spring Boot application is behind a proxy, and I've configured the X-Forwarded-* headers to be sent, and the Boot application is configured with server.forward-headers-strategy=FRAMEWORK, I would expect the path attribute in the default error response to reflect the forwarded path prefix, but it does not.

I have an extremely simple reproduction application at https://github.com/bpfoster/spring-boot-error-path-forwarded-prefix

Actuator includes the path prefix as expected:

$ curl http://localhost:8081/actuator -H 'X-Forwarded-Prefix: /api' | jq

{
  "_links": {
    "self": {
      "href": "http://localhost:8081/api/actuator",
      "templated": false
    },
    "health": {
      "href": "http://localhost:8081/api/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8081/api/actuator/health/{*path}",
      "templated": true
    }
  }
}

The error response does not:

$ curl http://localhost:8081/foobar -H 'X-Forwarded-Prefix: /api' | jq

{
  "timestamp": "2023-06-22T12:29:30.173+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Foobar",
  "path": "/foobar"
}

Note the hrefs in actuator include the X-Forwarded-Prefix value vs the path in the error do not.

Comment From: wilkinsona

Thanks for the sample, @bpfoster. The path in the error page is the value of the jakarta.servlet.error.request_uri request attribute so it would appear that forwarded header handling doesn't affect that attribute. Interestingly, the behavior is the same when using server.forward-headers-strategy=native. I'm not yet sure what this tells us. Things may be working as they should from a Servlet spec perspective or there may be a bug in both Framework and Tomcat.

Comment From: wilkinsona

Interestingly, the behavior is the same when using server.forward-headers-strategy=native

It's only the behavior on the error-handling side that is the same. The behaviour of the links endpoint changes because the native forwarded header handling in Tomcat (and Jetty and Undertow) does not support X-Forwarded-Prefix so this isn't a Tomcat bug.

When the jakarta.servlet.error.request_uri attribute is being set by Tomcat, the filter chain has complete unwound so the wrapping of the request and response that's done by ForwardedHeaderFilter is gone. This means that there's no opportunity to influence the URI that Tomcat sets as the attribute's value so this isn't a Framework bug either.

That leaves us with doing something in Boot or the application. One possibility is through a custom ErrorAttributes bean:

@Bean
DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes() {

        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
            Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
            addPath(errorAttributes, webRequest);
            return errorAttributes;
        }

        private void addPath(Map<String, Object> errorAttributes, WebRequest webRequest) {
            String path = getAttribute(webRequest, RequestDispatcher.ERROR_REQUEST_URI);
            if (path == null) {
                return;
            }
            HttpServletRequest servletRequest = ((ServletWebRequest)webRequest).getRequest();
            while (servletRequest instanceof HttpServletRequestWrapper wrapper) {
                servletRequest = (HttpServletRequest) wrapper.getRequest();
            }
            String pathPrefix = servletRequest.getHeader("X-Forwarded-Prefix");
            errorAttributes.put("path", (pathPrefix != null) ? pathPrefix + path: path);
        }

        @SuppressWarnings("unchecked")
        private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
            return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
        }

    };
}

This code assumes that you're using Framework's ForwardedHeaderFilter and, therefore, that X-Forwarded-Prefix will have been honored everywhere else. I'm not sure how easy it would be for us to detect this reliably in Boot's own code such that a change could be made to DefaultErrorAttributes itself. As such, this may just have to be worked around in application code.

I'll label the issue for discussion in a team meeting so that we can consider our options.

Comment From: bpfoster

Thanks for the response @wilkinsona! Tomcat not using X-Forwarded-Prefix was my understanding as well. X-Forwarded-For, -Host and -Proto appear to be fairly standard but -Prefix seems to be a more unique header, and is the main reason we are using the Framework forward-headers-strategy.

I do notice in DefaultErrorAttributes, that webRequest.getRequest() is an instance of ForwardedHeaderFilter$ForwardedHeaderExtractingRequest, so perhaps there is some way to make that detection that ForwardedHeaderFilter is in use.

I would think, if possible, it'd be better to have boot/framework automatically make this decoration in DefaultErrorAttributes (or elsewhere) rather than needing to do this in individual application code.

In the end, the path in the error response is probably not a big deal, but I did expect to see a consistent representation between these paths and that the error representation generated by Spring would have the same path handling as other points.

Comment From: wilkinsona

We talked about this today and realised that this could be addressed in Framework. When ForwardedHeaderFilter is called during the error dispatch, it could apply the value of the X-Forwarded-Prefix header to the value of the jakarta.servlet.error.request_uri request attribute. We'll transfer this to the Framework team so that they can consider such a change.

Comment From: rstoyanchev

We could set the jakarta.servlet.error.request_uri request attribute, but ideally should also restore it. This could be done from doFilter, but I see we sometimes recalculate from within calls to request.getRequestURI() in which case there is no good place to set and then restore.

I'm wondering if Boot could rely on request.getRequestURI() instead of the attribute?

Comment From: wilkinsona

Won't request.getRequestURI() return the URI for the forward to the error page? We need the URI for the original request that triggered the error. AFAIK, the jakarta.servlet.error.request_uri request attribute is the only way to get that.

Comment From: rstoyanchev

Indeed, I'll give it a try.

Comment From: rstoyanchev

Thanks for the sample @bpfoster. There is now a fix, which I have confirmed with the sample, but if you'd like to give it a try in your application, please use 6.1.2-SNAPSHOT.

Comment From: bpfoster

Hi @rstoyanchev, sorry for the delayed response. We've upgraded to framework 6.1.3 and I can confirm that I now see the path prefix in the error path. Thank you!