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

Jersey 2 fails if request contains Content-Type without a value #2883

Closed
najibk opened this issue Sep 4, 2018 · 12 comments
Closed

Jersey 2 fails if request contains Content-Type without a value #2883

najibk opened this issue Sep 4, 2018 · 12 comments

Comments

@najibk
Copy link

najibk commented Sep 4, 2018

Hello,

I'm using Jersey 2 to develop a web service on Jetty.
A POST request with an empty Content-type results in a 400 Bad Request response in Jetty 9.4.11.v20180605 without the request going through Request filters.
is this a bug related to this fix "Support empty HTTP header values" issues/1116 ?
Issue not present on Jetty 9.3.7.v20160115 for example.

My questions is how is this request handled ? ExceptionMapper does not catch this kinf of errors neither.

Thanks

@joakime
Copy link
Contributor

joakime commented Sep 5, 2018

Issue #1116 was corrected in version 9.4.7.v20170914

So what you are seeing in 9.4.11.v20160115 would be something new.
Can you capture the raw request (request line and header are sufficient, body content isn't needed)?
We want to see all of the details, if you can capture this in wireshark that would be the best option (then we can confirm other things like whitespace, delimiters, etc).

@joakime
Copy link
Contributor

joakime commented Sep 5, 2018

Tried putting together a testcase.

package jetty.badclient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class Issue2883EmptyContentType
{
    @SuppressWarnings("serial")
    public static class HelloServlet extends HttpServlet
    {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
        {
            resp.setContentType("text/plain");
            resp.getWriter().println("Hello Test");
            req.getParameterMap(); // trigger code that depends on Content-Type on the request
        }
    }
    
    private static Server server;
    
    @BeforeClass
    public static void startServer() throws Exception
    {
        server = new Server(8080);
        ServletContextHandler contexts = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        contexts.addServlet(HelloServlet.class,"/*");
        server.setHandler(contexts);

        server.start();
    }
    
    @AfterClass
    public static void stopServer() throws Exception
    {
        server.stop();
    }
    
    @Test
    public void testRequest() throws Throwable
    {
        String body = "hello";
        
        String request =
                "POST / HTTP/1.1\r\n" +
                "Host: localhost\r\n" + 
                "Content-Type: \r\n" +
                "Connection: close\r\n" + 
                "Accept-Encoding: gzip, deflated\r\n" +
                "Content-Length: " + body.length() + "\r\n" +
                "\r\n" +
                body;
        
        
        InetAddress destAddr = InetAddress.getByName("localhost");
        int port = 8080;

        SocketAddress endpoint = new InetSocketAddress(destAddr,port);

        try (Socket socket = new Socket())
        {
            socket.connect(endpoint);

            try (OutputStream out = socket.getOutputStream();
                    InputStream in = socket.getInputStream();
                    Reader reader = new InputStreamReader(in);
                    BufferedReader buf = new BufferedReader(reader))
            {
                // Send GET Request
                System.err.print(request);
                System.err.println();
                System.err.printf("Request: %,d bytes%n",request.length());
                out.write(request.getBytes(StandardCharsets.UTF_8));

                // Get Response
                String line = null;
                while ((line = buf.readLine()) != null)
                {
                    System.err.printf("[response]: %s%n",line);
                }
                System.err.println("[done]");
            }
        }

    }
}

Output shows ...

2018-09-05 14:37:10.132:INFO::main: Logging initialized @610ms to org.eclipse.jetty.util.log.StdErrLog
2018-09-05 14:37:10.430:INFO:oejs.Server:main: jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_65-b17
2018-09-05 14:37:10.549:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@20322d26{/,null,AVAILABLE}
2018-09-05 14:37:10.593:INFO:oejs.AbstractConnector:main: Started ServerConnector@13a5fe33{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2018-09-05 14:37:10.594:INFO:oejs.Server:main: Started @1073ms
POST / HTTP/1.1
Host: localhost
Content-Type: 
Connection: close
Accept-Encoding: gzip, deflated
Content-Length: 5

hello
Request: 128 bytes
[response]: HTTP/1.1 200 OK
[response]: Connection: close
[response]: Date: Wed, 05 Sep 2018 19:37:10 GMT
[response]: Content-Type: text/plain;charset=iso-8859-1
[response]: Content-Length: 11
[response]: Server: Jetty(9.4.11.v20180605)
[response]: 
[response]: Hello Test
[done]
2018-09-05 14:37:10.801:INFO:oejs.AbstractConnector:main: Stopped ServerConnector@13a5fe33{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2018-09-05 14:37:10.804:INFO:oejsh.ContextHandler:main: Stopped o.e.j.s.ServletContextHandler@20322d26{/,null,UNAVAILABLE}

Simply having an empty Content-Type means nothing to Jetty, even when using features of the servlet spec that require parsing of the Content-Type header.

Something else is going on.

@najibk
Copy link
Author

najibk commented Sep 5, 2018

Thanks for your reply.

The API would look something like this :

@POST
@Path("/vis")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response postRessource(Body body){}

wireshark

nginx does not alter the response :) as you can see from the body message,

I am using jackson for json manipulation, but I doubt that would be it as in the previous version of jetty mentioned above, the request does go through ContainerRequestFilter as expected.

@janbartel
Copy link
Contributor

@najibk turn on DEBUG log level for org.eclipse.jetty.servlet.ServletHandler. You should see log lines related to the handling of the request as it passes through the filterchain (if there is one).

Can you also please provide a small reproduction test case.

@najibk
Copy link
Author

najibk commented Sep 6, 2018

Here's a full example to reproduce the issue :

API :

@POST
    @Path("vis")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response addResource(){
        return Response.ok().build();
    }

Filter (will never be called anyway) :

@Provider
public class RequestFilter implements ContainerRequestFilter {

    /**
     * Add request id and service id to Thread context for automatic logging
     * @param containerRequestContext
     */
    @Override
    public void filter(ContainerRequestContext containerRequestContext){
        System.out.println("Hello filter");
    }
}

Test :

@Test
    public void testRequest() throws Throwable
    {
        String body = "hello";

        String request =
                "POST /vis HTTP/1.1\r\n" +
                        "Host: localhost\r\n" +
                        "Content-Type: \r\n" +
                        "Connection: close\r\n" +
                        "Accept-Encoding: gzip, deflated\r\n" +
                        "Content-Length: " + body.length() + "\r\n" +
                        "\r\n" +
                        body;


        InetAddress destAddr = InetAddress.getByName("localhost");
        int port = 8080;

        SocketAddress endpoint = new InetSocketAddress(destAddr,port);

        try (Socket socket = new Socket())
        {
            socket.connect(endpoint);

            try (OutputStream out = socket.getOutputStream();
                 InputStream in = socket.getInputStream();
                 Reader reader = new InputStreamReader(in);
                 BufferedReader buf = new BufferedReader(reader))
            {
                // Send GET Request
                System.err.print(request);
                System.err.println();
                System.err.printf("Request: %,d bytes%n",request.length());
                out.write(request.getBytes(StandardCharsets.UTF_8));

                // Get Response
                String line = null;
                while ((line = buf.readLine()) != null)
                {
                    System.err.printf("[response]: %s%n",line);
                }
                System.err.println("[done]");
            }
        }

    }

DEBUG logs :
jettylogs.TXT

@janbartel
Copy link
Contributor

@najibk I'd need a webapp as a reproduction.

Also, the debug log shows that there is no filter chain. So please set Server.dumpAfterStart(true) on your server instance (if running embedded, or use the dumpAfterStart property in the server.ini file if running from the distro). That will show you what filters have been configured.

@joakime
Copy link
Contributor

joakime commented Sep 11, 2018

Jersey will throw the response 400 in many scenarios related to invalid/unexpected Content-Type and POST requests with empty/unexpected bodies.
The fact that you have @Consumes(MediaType.APPLICATION_JSON) on your API means that an empty Content-Type will not match that API.
What you are experiencing is likely being produced by Jersey.
Have you filed this issue with the https://github.com/eclipse-ee4j/jersey project?

Also, your code comment Add request id and service id to Thread context for automatic logging point to a lack of understanding of threading on Jetty.
A single request can use 1..n threads in its lifetime, there's nothing about handling a request that says it must only use 1 thread.
Using Thread Local or Thread Context to attempt to track behavior will not work reliably.

@najibk
Copy link
Author

najibk commented Sep 11, 2018

Thanks @joakime for your reply,

The main issue is not the response itself but rather the way the request is handled, as I said before, I can't handle Exceptions anymore with the recent version of Jetty, catching exceptions is useful for editing the response body for example with a more explicit message.
I have not filed this issue to jersey as of yet, because I thought it's more likely that it's Jetty's behaviour that changed from version 9.3.7.v20160115 to 9.4.11.v20180605.

As to my comment, it's only for logging purposes : response status, duration ... i did not attach the full code to focus on the main issue.

@joakime
Copy link
Contributor

joakime commented Sep 11, 2018

Confirmed.
I setup java.util.logging and set org.glassfish.jersey.servlet to level FINE, the following is produced.

This is a Jersey behavior.

POST /vis HTTP/1.1
Host: localhost
Content-Type: 
Connection: close
Accept-Encoding: gzip, deflated
Content-Length: 5

hello
Request: 131 bytes
[FINE] WebComponent: Attempt to read the header value failed.
org.glassfish.jersey.message.internal.HeaderValueException: Unable to parse "Content-Type" header value: ""
	at org.glassfish.jersey.message.internal.InboundMessageContext.exception(InboundMessageContext.java:338)
	at org.glassfish.jersey.message.internal.InboundMessageContext.singleHeader(InboundMessageContext.java:333)
	at org.glassfish.jersey.message.internal.InboundMessageContext.getMediaType(InboundMessageContext.java:446)
	at org.glassfish.jersey.servlet.WebComponent.filterFormParameters(WebComponent.java:612)
	at org.glassfish.jersey.servlet.WebComponent.initContainerRequest(WebComponent.java:463)
	at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:414)
	at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:370)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:389)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:342)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:229)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:857)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:535)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1595)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1340)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:473)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1564)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1242)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
	at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:61)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
	at org.eclipse.jetty.server.Server.handle(Server.java:503)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
	at java.lang.Thread.run(Thread.java:748)
Caused by: javax.ws.rs.ProcessingException: java.lang.IllegalArgumentException: Error parsing media type ''
	at org.glassfish.jersey.message.internal.InboundMessageContext$5.apply(InboundMessageContext.java:452)
	at org.glassfish.jersey.message.internal.InboundMessageContext$5.apply(InboundMessageContext.java:446)
	at org.glassfish.jersey.message.internal.InboundMessageContext.singleHeader(InboundMessageContext.java:331)
	... 34 more
Caused by: java.lang.IllegalArgumentException: Error parsing media type ''
	at org.glassfish.jersey.message.internal.MediaTypeProvider.fromString(MediaTypeProvider.java:93)
	at org.glassfish.jersey.message.internal.MediaTypeProvider.fromString(MediaTypeProvider.java:61)
	at javax.ws.rs.core.MediaType.valueOf(MediaType.java:196)
	at org.glassfish.jersey.message.internal.InboundMessageContext$5.apply(InboundMessageContext.java:450)
	... 36 more
Caused by: java.text.ParseException: End of header.
	at org.glassfish.jersey.message.internal.HttpHeaderReaderImpl.getNextCharacter(HttpHeaderReaderImpl.java:179)
	at org.glassfish.jersey.message.internal.HttpHeaderReaderImpl.next(HttpHeaderReaderImpl.java:140)
	at org.glassfish.jersey.message.internal.HttpHeaderReaderImpl.next(HttpHeaderReaderImpl.java:135)
	at org.glassfish.jersey.message.internal.HttpHeaderReader.nextToken(HttpHeaderReader.java:128)
	at org.glassfish.jersey.message.internal.MediaTypeProvider.valueOf(MediaTypeProvider.java:111)
	at org.glassfish.jersey.message.internal.MediaTypeProvider.fromString(MediaTypeProvider.java:91)
	... 39 more
[response]: HTTP/1.1 400 Bad Request
[response]: Connection: close
[response]: Date: Tue, 11 Sep 2018 12:53:48 GMT
[response]: Cache-Control: must-revalidate,no-cache,no-store
[response]: Content-Type: text/html;charset=iso-8859-1
[response]: Content-Length: 324
[response]: Server: Jetty(9.4.12.v20180830)
[response]: 
[response]: <html>
[response]: <head>
[response]: <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
[response]: <title>Error 400 Bad Request</title>
[response]: </head>
[response]: <body><h2>HTTP ERROR 400</h2>
[response]: <p>Problem accessing /vis. Reason:
[response]: <pre>    Bad Request</pre></p><hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.4.12.v20180830</a><hr/>
[response]: 
[response]: </body>
[response]: </html>

You are sending Content-Type: with an empty value, when jersey uses HttpServletRequest.getHeader("Content-Type") you'll get an empty string back.

An empty string "" indicates that the header exists, but has no value.
If HttpServletRequest.getHeader(String name) returned null then that would indicate that the header itself did not exist.

@joakime
Copy link
Contributor

joakime commented Sep 11, 2018

A full demo project (including above test case) can be found at ...
https://github.com/joakime/jetty-issue-2883-jersey-empty-content-type

@najibk
Copy link
Author

najibk commented Sep 11, 2018

Thank you @joakime for the detailed response and the demo project, I have filed an issue as you suggested to eclipse-ee4j/jersey, it can be found at eclipse-ee4j/jersey#3932.

@joakime
Copy link
Contributor

joakime commented Sep 11, 2018

I'm closing this as not a Jetty issue.
This is either an issue with your user-agent (shouldn't be sending the Content-Type header with an empty value).
Or it's a Jersey issue (it should recognize that headers can exist but have an empty value vs not existing, which has two different meanings)

@joakime joakime closed this as completed Sep 11, 2018
@joakime joakime changed the title Jetty 9.4.11 results in request not going through Filters with Jersey 2 Jersey 2 fails if request contains Content-Type without a value Sep 11, 2018
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

3 participants