Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only request bodies with content-type multipart/* and application/x-www-form-urlencoded work #3418

Open
candrews opened this issue Mar 14, 2019 · 13 comments

Comments

@candrews
Copy link
Contributor

Make a request through zuul using a POST or PUT verb with a content-type that is not multipart/* or application/x-www-form-urlencoded`

When Zuul makes the request to the backend, it won't include the request body.

The reason is that the original request body is consumed in org.springframework.cloud.netflix.zuul.filters.TraceProxyRequestHelper.debug(String, String, MultiValueMap<String, String>, MultiValueMap<String, String>, InputStream) leaving no request body to consume when org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter actually sends the request.

Requests of content-type application/x-www-form-urlencoded work because org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter gets the body. Requests of type multipart/* work because org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper.shouldDebugBody(RequestContext) explicitly excludes them.

To reproduce the issue, just make a request through zuul with a Content-Type of text/plain, for example, and you'll always get a time out error as the backend server will keep waiting for response body that it will never get.

I believe this issue was originally reported at #2610

@spencergibb
Copy link
Member

I'm unable to reproduce this with a POST with text/plain.

Can you provide a complete, minimal, verifiable sample that reproduces the problem? It should be available as a GitHub (or similar) project or attached to this issue as a zip file.

@candrews
Copy link
Contributor Author

My apologies if this isn't that completely minimal, but it does reproduce the problem and it's really easy to run.

Grab this:
https://www.integralblue.com/zuul3418.tar.gz

And run: ./mvnw spring-boot:run -Dspring-boot.run.profiles=localhost

See the problem with:
curl -H 'Content-Type: abc/xyz' --data "stuff" http://localhost:8080/TEST.php

Now see how the problem is fixed by editing src/main/resources/application.properties and uncommenting the zuul.trace-request-body=false line at the bottom.

and trying that same curl command again.

@spencergibb
Copy link
Member

$ curl -H 'Content-Type: abc/xyz' --data "stuff" http://localhost:8080/TEST.php                                                                                                    (9121)[14:19:53]
{"timestamp":1552673996272,"status":404,"error":"Not Found","message":"No message available","path":"/TEST.php"}   

I'm going to ask that you spend some time making a minimal app I can use.

@candrews
Copy link
Contributor Author

The problem is that the request's InputStream is being read multiple times which doesnt' work as Zuul is expecting unless the request is wrapped to specifically implement that behavior. Zuul is expecting to call ServletRequest.getInputStream() and each time get a different InputStream instance over the same data; but, the base ServletRequest provided by the application server doesn't do that - it returns the same InputStream instance each time which means it can only be read once.

If the request's content type is application/x-www-form-urlencoded, the request is wrapped by WorkaroundFormBodyWrapperFilter and its getInputStream() implementation returns a new InputStream of the data each time it's called: https://github.com/spring-cloud/spring-cloud-netflix/blob/v2.1.1.RELEASE/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java#L180

If the request's content contains "multipart", see https://github.com/spring-cloud/spring-cloud-netflix/blob/v2.1.1.RELEASE/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java#L273, then Zuul doesn't debug it, avoiding this issue.

So if the content type isn't application/x-www-form-urlencoded or multipart, then the request isn't wrapped and it will be consumed at https://github.com/spring-cloud/spring-cloud-netflix/blob/blob/v2.1.1.RELEASE/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/TraceProxyRequestHelper.java#L129 Once the trace/debugger consume it, when Zuul goes to read the InputStream, there's no data to read off it.

@spencergibb
Copy link
Member

I understand your reasoning but was unable to replicate the described behaviour with a minimal app.

I couldn't get your app to behave

I'm going to ask that you spend some time making a minimal app I can use.

@candrews
Copy link
Contributor Author

candrews commented Mar 18, 2019

Here's a minimal app: https://www.integralblue.com/zuul3418-2.tar.gz
Run ./mvnw spring-boot:run
and see the problem with:
curl -H 'Content-Type: abc/xyz' --data "stuff" http://localhost:8080/TEST.php
You'll see a long delay, and then a stack trace. That's the problem.

I've found that this issue is only reproducible when zuul.use-filter=true and zuul.servlet-path=/. When both are set that way, at https://github.com/spring-cloud/spring-cloud-netflix/blob/v2.1.1.RELEASE/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/TraceProxyRequestHelper.java#L87 requestEntity is an instance of org.apache.catalina.connector.CoyoteInputStream. When they are not set that way, requestEntity is an instance of com.netflix.zuul.http.ServletInputStreamWrapper which explains the behavior as ServletInputStreamWrapper does allow getInputStream() to be called multiple times while CoyoteInputStream does not.

@candrews
Copy link
Contributor Author

Tracing a bit, I noticed that when this occurs, at https://github.com/spring-cloud/spring-cloud-netflix/blob/v2.1.1.RELEASE/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java#L74 request is a org.apache.catalina.connector.RequestFacade which is not an instance of HttpServletRequestWrapper, and RequestUtils.isDispatcherServletRequest() returns false, so the method returns null.

One solution would be to change this method to remove the RequestUtils.isDispatcherServletRequest() so it always buffers the body.

@spencergibb
Copy link
Member

If it only happens with zuul.use-filter=true, then why use it?

Removing the call to RequestUtils.isDispatcherServletRequest() is not an option as we have well-documented ways to NOT buffer.

@candrews
Copy link
Contributor Author

If it only happens with zuul.use-filter=true, then why use it?

I think those reasons are covered in #2887

I like the idea of not buffering, don't get me wrong - but I also want to work :) Perhaps the buffering decision can be made based on the value of the zuul.trace-request-body setting as well?

@spencergibb
Copy link
Member

I'm not keen to add make a buffering decision based off of something that historically hasn't had anything to do with buffering.

So far the only user of zuul.use-filter=true I know of is you. I'd rather turn off trace request body if zuul.use-filter=true.

@candrews
Copy link
Contributor Author

I'd rather turn off trace request body if zuul.use-filter=true.

That seems reasonable. Should trace request body be off by default always?

On a related note, https://github.com/spring-cloud/spring-cloud-netflix/blob/2.0.x/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java has this comment:

// TODO: only if in servlet 3.0 env

and its class level javadocs is:

/**
 * Pre {@link ZuulFilter} that wraps requests in a Servlet 3.0 compliant wrapper.
 * Zuul's default wrapper is only Servlet 2.5 compliant.
 * @author Spencer Gibb
 */

Since spring-cloud-netflix-zuul runs on Spring Boot 2.0, and Spring Boot 2.0 requires Servlet 3.1 (see https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/reference/html/getting-started-system-requirements.html), it seems that filter should either be removed (since it's no longer necessary), renamed, or its use replaced by using Spring's org.springframework.web.util.ContentCachingRequestWrapper.

@spencergibb
Copy link
Member

Should trace request body be off by default always?

Probably, but we won't be making a breaking change anymore.

The problem was the zuul HttpServletRequestWrapper didn't implement servlet 3.0 methods. Not sure if that was ever fixed. That would be the only reason to remove it, not what boot or spring defaults to.

@spencergibb
Copy link
Member

Should trace request body be off by default always?

Probably, but we won't be making a breaking change anymore.

After thinking about it, I'd be ok making this change for Hoxton (2.2.x)

candrews added a commit to candrews/spring-cloud-netflix that referenced this issue Mar 19, 2019
traceRequestBody requires the body to be buffered which reduces performance and can be problematic, such as when used in combination with `zuul.use-filter=true` and `zuul.servlet-path=/` See spring-cloud#3418

traceRequestBody should only be set when logging the request body is actually necessary.
candrews added a commit to candrews/spring-cloud-netflix that referenced this issue Mar 19, 2019
Request body tracing reads the body then it will be read again later for actual handling.
Therefore, request buffering is required when request body tracing.

Fixes spring-cloud#3418
candrews added a commit to candrews/spring-cloud-netflix that referenced this issue Mar 19, 2019
traceRequestBody requires the body to be buffered which reduces performance and can be problematic, such as when used in combination with `zuul.use-filter=true` and `zuul.servlet-path=/` See spring-cloud#3418

traceRequestBody should only be set when logging the request body is actually necessary.
spencergibb pushed a commit that referenced this issue Mar 19, 2019
traceRequestBody requires the body to be buffered which reduces performance and can be problematic, such as when used in combination with `zuul.use-filter=true` and `zuul.servlet-path=/` See #3418

traceRequestBody should only be set when logging the request body is actually necessary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants