Handling POST request via Socket in Java

10,702

The code you have shown is not the correct way to read HTTP requests.

First off, Java has its own HttpServer and HttpsServer classes. You should consider using them.

Otherwise, you have to implement the HTTP protocol manually. You need to read the input line-by-line until you reach an empty line indicating the end of the request headers, then look at the headers you have read, in particular the Transfer-Encoding and Content-Length headers, to know how to read the remaining bytes of the request, per RFC 2616 Section 4.4:

4.4 Message Length

The transfer-length of a message is the length of the message-body as it appears in the message; that is, after any transfer-codings have been applied. When a message-body is included with a message, the transfer-length of that body is determined by one of the following (in order of precedence):

  1. Any response message which "MUST NOT" include a message-body (such as the 1xx, 204, and 304 responses and any response to a HEAD request) is always terminated by the first empty line after the header fields, regardless of the entity-header fields present in the message.
  1. If a Transfer-Encoding header field (section 14.41) is present and has any value other than "identity", then the transfer-length is defined by use of the "chunked" transfer-coding (section 3.6), unless the message is terminated by closing the connection.
  1. If a Content-Length header field (section 14.13) is present, its decimal value in OCTETs represents both the entity-length and the transfer-length. The Content-Length header field MUST NOT be sent if these two lengths are different (i.e., if a Transfer-Encoding header field is present). If a message is received with both a Transfer-Encoding header field and a Content-Length header field, the latter MUST be ignored.
  1. If the message uses the media type "multipart/byteranges", and the ransfer-length is not otherwise specified, then this self- elimiting media type defines the transfer-length. This media type UST NOT be used unless the sender knows that the recipient can arse it; the presence in a request of a Range header with ultiple byte- range specifiers from a 1.1 client implies that the lient can parse multipart/byteranges responses.

A range header might be forwarded by a 1.0 proxy that does not understand multipart/byteranges; in this case the server MUST delimit the message using methods defined in items 1,3 or 5 of this section.

  1. By the server closing the connection. (Closing the connection cannot be used to indicate the end of a request body, since that would leave no possibility for the server to send back a response.)

For compatibility with HTTP/1.0 applications, HTTP/1.1 requests containing a message-body MUST include a valid Content-Length header field unless the server is known to be HTTP/1.1 compliant. If a request contains a message-body and a Content-Length is not given, the server SHOULD respond with 400 (bad request) if it cannot determine the length of the message, or with 411 (length required) if it wishes to insist on receiving a valid Content-Length.

All HTTP/1.1 applications that receive entities MUST accept the "chunked" transfer-coding (section 3.6), thus allowing this mechanism to be used for messages when the message length cannot be determined in advance.

Messages MUST NOT include both a Content-Length header field and a non-identity transfer-coding. If the message does include a non- identity transfer-coding, the Content-Length MUST be ignored.

When a Content-Length is given in a message where a message-body is allowed, its field value MUST exactly match the number of OCTETs in the message-body. HTTP/1.1 user agents MUST notify the user when an invalid length is received and detected.

Try something more like this (semi-pseudo code):

String readLine(BufferedInputStream in)
{
    // HTTP carries both textual and binary elements.
    // Not using BufferedReader.readLine() so it does
    // not "steal" bytes from BufferedInputStream...

    // HTTP itself only allows 7bit ASCII characters
    // in headers, but some header values may be
    // further encoded using RFC 2231 or 5987 to
    // carry Unicode characters ...

    InputStreamReader r = new InputStreamReader(in, StandardCharsets.US_ASCII);
    StringBuilder sb = new StringBuilder();
    char c;
    while ((c = r.read()) >= 0) {
        if (c == '\n') break;
        if (c == '\r') {
            c = r.read();
            if ((c < 0) || (c == '\n')) break;
            sb.append('\r');
        }
        sb.append(c);
    }
    return sb.toString();
}

...

BufferedInputStream in = new BufferedInputStream(clientSocket.getInputStream());

String request = readLine(in);
// extract method, resource, and version...

String line;

do
{
    line = readLine(in);
    if (line.isEmpty()) break;
    // store line in headers list...
}
while (true);

// parse headers list...

if (request method has a message-body) // POST, etc
{
    if ((request version >= 1.1) &&
        (Transfer-Encoding header is present) &&
        (Transfer-Encoding != "identity"))
    {
        // read chunks...
        do
        {
            line = readLine(in); // read chunk header
            int size = extract value from line;
            if (size == 0) break;
            // use in.read() to read the specified
            // number of bytes into message-body...
            readLine(in); // skip trailing line break
        }
        while (true);

        // read trailing headers...
        line = readLine(in);
        while (!line.isEmpty())
        {
            // store line in headers list, updating
            // any existing header as needed...
        }

        // parse headers list again ...
    }
    else if (Content-Length header is present)
    {
        // use in.read() to read the specified
        // number of bytes into message-body...
    }
    else if (Content-Type is "multipart/...")
    {
        // use readLine(in) and in.read() as needed
        // to read/parse/decode MIME encoded data into
        // message-body until terminating MIME boundary
        // is reached...
    }
    else
    {
        // fail the request...
    }
}

// process request and message-body as needed..
Share:
10,702
Fabian Ochmann
Author by

Fabian Ochmann

Updated on June 04, 2022

Comments

  • Fabian Ochmann
    Fabian Ochmann almost 2 years

    I'm trying to handle a simple POST Request in Java using a Socket. I can receive the request header and answer the request without any problem, but I certainly can not get the body of the request.

    I read somewhere that I'd need to open a second InputStream to achive this, but this doesn't really makes sense to me. Do you have any tips on how to get the request body?

    This is what I basically use to get the header:

    BufferedReader in = new BufferedReader(new InputStreamReader(
                    clientSocket.getInputStream()));
    
    char[] inputBuffer = new char[INPUT_BUFFER_LENGTH];
    
    int inputMessageLength = in.read(inputBuffer, 0,
                    INPUT_BUFFER_LENGTH);
    
    String inputMessage = new String(inputBuffer, 0, inputMessageLength);
    

    So, the message I get is something like:

    POST / HTTP/1.1
    User-Agent: Java/1.8.0_45
    Host: localhost:5555
    Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
    

    But I can't get the parameters of the POST request.

    Edit:

    So it turned out I just had INPUT_BUFFER_LENGTH up high enough (I know, shame on me). So as it worked I changed my ServerSocket to SSLServerSocket and tried again to send a request with a HttpsUrlConnection from Java, now I have the same problem again (already checked the buffer), getting something like this:

    POST / HTTP/1.1
    User-Agent: Java/1.8.0_45
    Host: localhost:5555
    Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
    Connection: keep-alive
    Content-type: application/x-www-form-urlencoded
    Content-Length: 128
    
    *Missing Body*
    

    It turned out I only get this when sending requests with my Java-Client - Sending requests from Chrome, etc are working fine - so I assume I got something wrong in my code. This is what I use to send the request:

    System.setProperty("javax.net.ssl.trustStore", ...);
    System.setProperty("javax.net.ssl.trustStorePassword", ...);
    
    SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory
                .getDefault();
    
    String url = "https://...";
    URL obj = new URL(url);
    HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();
    
    HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);
    
    con.setRequestMethod("POST");
    con.setDoOutput(true);
    
    OutputStreamWriter writer = new OutputStreamWriter(con.getOutputStream());
    
    writer.write(*Some String*);
    writer.flush();
    writer.close();
    

    Any tips on what might be wrong with my code?

  • Fabian Ochmann
    Fabian Ochmann almost 9 years
    Thanks for all the information. Turned out that in my case I just had to increase the value of my buffer, as I only sent plain text. But I guess in case of sending raw bytes I'll have to do it "the hard way" like you suggested.
  • Remy Lebeau
    Remy Lebeau almost 9 years
    It doesn't matter what the content is, or how long it is. HTTP is a protocol, you have to follow the protocol in order to receive the content correctly. Java has its own HttpServer and HttpsServer classes. If you don't want to use them, you have to implement the protocol yourself.
  • Fabian Ochmann
    Fabian Ochmann almost 9 years
    I ended up implementing this the way you said, thanks for your help!