I had a class: MockServerHandler
I use in my itests using Jetty 9 which I had to migrate to Jetty 12.
Before the migration (Jetty 9):
public class MockServerHandler extends DefaultHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
try {
log.debug("Got request for: {} request type {} ", target, request.getMethod());
handleDownload(request, response);
} catch (Exception e) {
log.error("Mock handler caught and exception", e);
}
}
private void handleDownload(HttpServletRequest request, HttpServletResponse response) {
byte[] byteBuffer = getContentToReturn();
long lastModified = getLastModified();
String contentType = getContentType();
int returnCode = getReturnCode();
response.setStatus(returnCode);
if (lastModified > 0L) {
response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(), lastModified);
}
response.setContentType(contentType);
response.setContentLength(byteBuffer.length);
OutputStream outputStream = response.getOutputStream();
outputStream.write(byteBuffer, 0, byteBuffer.length);
response.flushBuffer();
outputStream.close();
}
}
After bumping to Jetty 12:
public class MockServerHandler extends DefaultHandler {
@Override
public boolean handle(Request request, Response response, Callback callback)
throws IOException, ServletException {
try {
String target = request.getHttpURI().getPath();
log.debug("Got request for: {} request type {} ", target, request.getMethod());
handleDownload(request, response);
callback.succeeded();
} catch (Exception e) {
callback.failed(e);
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
log.error("Mock handler caught and exception", e);
}
return true;
}
private void handleDownload(Request request, Response response) {
byte[] byteBuffer = getContentToReturn();
long lastModified = getLastModified();
String contentType = getContentType();
int returnCode = getReturnCode();
response.setStatus(returnCode);
if (lastModified > 0L) {
response.getHeaders().addDateField(HttpHeader.LAST_MODIFIED.asString(), lastModified);
}
response.getHeaders().add(HttpHeader.CONTENT_TYPE, contentType);
response.getHeaders().add(HttpHeader.CONTENT_LENGTH, byteBuffer.length);
OutputStream outputStream = Content.Sink.asOutputStream(response);
outputStream.write(byteBuffer, 0, byteBuffer.length);
outputStream.flush();
outputStream.close();
}
}
After these changes my integration tests started to fail. Some due to read timeout, others expect UTF-8 encoded response body and suddenly fail complaining the first byte isn't compliant, and more. I'm clearly missing something, as I tried to make the least amount of changes possible to retain behavior.
I noticed DefaultHandler
passes super(InvocationType.NON_BLOCKING);
which I did not expect (it wasn't non-blocking on Jetty 9, right?). I tried extending Handler.Abstract
directly but that didn't change anything.
Am I violating the new Jetty contract somehow? I've read the docs carefully but couldn't see anything I am doing blatantly wrong. Any advice to understand how it works and what I'm missing is very much appreciated.
If you have a complete byte[]
array of your content, don't use a blocking OutputStream
, just send it as is, there's no need to get overly complicated with the OutputStream
class.
Your Jetty 12 implementation doesn't even handle content-length properly.
Separate your concerns.
The minute you mix these you will have a messy set of code that becomes hard to maintain.
package handlers;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class MockDownloadHandler extends Handler.Abstract
{
private static final Logger LOG = LoggerFactory.getLogger(MockDownloadHandler.class);
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
String target = Request.getPathInContext(request);
LOG.debug("Got request for: {} request type {} ", target, request.getMethod());
// Get response content
ByteBuffer responseBody;
try
{
responseBody = ByteBuffer.wrap(getContentToReturn());
}
catch (Throwable e)
{
callback.failed(e);
return true; // you handled the failure.
}
// Prepare response metadata
long lastModified = getLastModified();
// The contentType string here should have the `;charset=` section defined too.
// Example: "text/html;charset=utf-8" or "text/html;charset=iso8859-1"
// Note: not all content-types support a `;charset=` here, some are implied.
// for example json is implied to be `utf-8` but never reported in this
// content-type as `;charset=utf-8`, that would, in fact, be a violation
// of the json spec to include the `;charset=` entry.
String contentType = getContentType();
int returnCode = getReturnCode();
response.setStatus(returnCode);
response.getHeaders().add(HttpHeader.CONTENT_TYPE, contentType);
// If you leave this out, you are subject to possible Transfer-Encoding: chunked responses.
response.getHeaders().add(HttpHeader.CONTENT_LENGTH, responseBody.remaining());
if (lastModified > 0)
response.getHeaders().addDateField(HttpHeader.LAST_MODIFIED.asString(), lastModified);
// Send the response
response.write(true, responseBody, callback);
return true;
}
protected abstract int getReturnCode();
protected abstract String getContentType();
protected abstract long getLastModified();
public byte[] getContentToReturn()
{
// Be careful of proper conversion to bytes.
// If the data is already binary, no need to do anything to encode it.
// But if the data starts out as text, the conversion to bytes needs to be encoded
// based on the same charset that you are going to report back on the `Content-Type`
// response header.
// Example: a string to byte conversion using UTF-8
return "Hello € euro".getBytes(StandardCharsets.UTF_8);
}
}
You probably want your getContentToReturn()
step also be mime-type and charset aware, so that you can perform the correct behaviors to the content conversion to bytes, and also carry over the proper content-type (with charset) too.
Also be aware of MimeTypes
object, it can do a few things for you.
Like let you know about assumed vs inferred charsets.
Example.
package mimetypes;
import java.nio.charset.Charset;
import org.eclipse.jetty.http.MimeTypes;
public class MimeTypeDemo
{
public static void main(String[] args)
{
MimeTypes mimeTypes = new MimeTypes();
dump(mimeTypes, "text/html");
dump(mimeTypes, "text/html;charset=iso8859-1");
dump(mimeTypes, "image/jpeg");
dump(mimeTypes, "application/json");
dump(mimeTypes, "application/json;charset=UTF-8");
dump(mimeTypes, "application/json;charset=us-ascii");
}
public static void dump(MimeTypes mimeTypes, String applicationSpecifiedContentType)
{
System.out.printf("%n-- dump: \"%s\" --%n", applicationSpecifiedContentType);
System.out.printf(" mime-type = %s%n", MimeTypes.getContentTypeWithoutCharset(applicationSpecifiedContentType));
System.out.printf(" charset (string) = %s%n", MimeTypes.getCharsetFromContentType(applicationSpecifiedContentType));
System.out.printf(" charset (class) = %s%n", mimeTypes.getCharset(applicationSpecifiedContentType));
System.out.printf(" assumed charset = %s%n", mimeTypes.getAssumedCharset(applicationSpecifiedContentType));
System.out.printf(" inferred charset = %s%n", mimeTypes.getInferredCharset(applicationSpecifiedContentType));
System.out.printf(" charset inferred from content-type = %s%n", mimeTypes.getCharsetInferredFromContentType(applicationSpecifiedContentType));
String contentTypeHeaderValue = toHeaderValue(mimeTypes, applicationSpecifiedContentType);
System.out.printf(" Content-Type (clean header to send) = %s%n", contentTypeHeaderValue);
}
private static String toHeaderValue(MimeTypes mimeTypes, String rawContentType)
{
// Get the mimetype from the raw content-type string
String mimeType = MimeTypes.getContentTypeWithoutCharset(rawContentType);
// Figure out the charset, normalized if found, add if missing and not assumed.
String rawCharset = MimeTypes.getCharsetFromContentType(rawContentType);
if (rawCharset == null)
{
Charset inferredCharset = mimeTypes.getInferredCharset(mimeType);
if (inferredCharset != null)
rawCharset = inferredCharset.name();
}
if (rawCharset != null)
return String.format("%s;charset=%s", mimeType, rawCharset);
else
return mimeType;
}
}
With output of ...
-- dump: "text/html" --
mime-type = text/html
charset (string) = null
charset (class) = UTF-8
assumed charset = null
inferred charset = UTF-8
charset inferred from content-type = UTF-8
Content-Type (clean header to send) = text/html;charset=UTF-8
-- dump: "text/html;charset=iso8859-1" --
mime-type = text/html
charset (string) = iso8859-1
charset (class) = ISO-8859-1
assumed charset = null
inferred charset = null
charset inferred from content-type = null
Content-Type (clean header to send) = text/html;charset=iso8859-1
-- dump: "image/jpeg" --
mime-type = image/jpeg
charset (string) = null
charset (class) = null
assumed charset = null
inferred charset = null
charset inferred from content-type = null
Content-Type (clean header to send) = image/jpeg
-- dump: "application/json" --
mime-type = application/json
charset (string) = null
charset (class) = UTF-8
assumed charset = UTF-8
inferred charset = null
charset inferred from content-type = null
Content-Type (clean header to send) = application/json
-- dump: "application/json;charset=UTF-8" --
mime-type = application/json
charset (string) = utf-8
charset (class) = UTF-8
assumed charset = null
inferred charset = null
charset inferred from content-type = null
Content-Type (clean header to send) = application/json;charset=utf-8
-- dump: "application/json;charset=us-ascii" --
mime-type = application/json
charset (string) = us-ascii
charset (class) = US-ASCII
assumed charset = null
inferred charset = null
charset inferred from content-type = null
Content-Type (clean header to send) = application/json;charset=us-ascii
This shows now the application provided Content-Type (with optional charset) can be interrogated and also resolved into a string that is appropriate to use on HTTP protocol for the Content-Type
header value.