Magnus Magnus - 3 months ago 92
Android Question

Could find no Content-Disposition header within part

I'm trying to upload a picture file from my Android application to my JavaEE REST service, which is deployed to a JBoss Wildfly 9 server.

My understanding of

Content-Disposition
is that it should be defined as its own header for each part uploaded, but also can be defined in the request header - if only one file is uploaded.

So in the header I've defined
Content-Type: multipart/form-data;boundary=*****
, while each (for now only one) part starts with
--*****
, before I end the request body with
--*****--
. This is in case I need to upgrade to multiple file upload on a later point.

The server should be accessible from both Android and AngularJS applications. Therefore I've added a
ContainerResponseFilter
for the AngularJS app, with the following, but I don't see any reason this should be the reason for the blocked request.

@Override
public void filter(ContainerRequestContext requestCtx, ContainerResponseContext responseCtx)
throws IOException {
responseCtx.getHeaders().add("Access-Control-Allow-Origin", "http://localhost:8000");
responseCtx.getHeaders().add("Access-Control-Allow-Credentials", "true");
responseCtx.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
responseCtx.getHeaders().add("Access-Control-Allow-Headers", "Host, "
+ "Accept, "
+ "Origin, "
+ "Connection, "
+ "Content-Type, "
+ "Cache-Control, "
+ "Content-Length, "
+ "Accept-Encoding, "
+ "Content-Disposition");
}





@Override
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
String reqSource = "(" + servletRequest.getRemoteAddr() + ") "
+ servletRequest.getRemoteUser() + "@"
+ servletRequest.getRemoteHost() + ":"
+ servletRequest.getRemotePort();
LOGGER.trace(" :: Source :: [{}]", reqSource);

String userId = (securityContext.getUserPrincipal() != null ?
securityContext.getUserPrincipal().getName() : "unknown");
LOGGER.trace(" :: User :: [{}]", userId);

String reqUri = servletRequest.getRequestURI();
String reqType = servletRequest.getMethod();
LOGGER.trace(" :: Boundary :: [{}] - [{}]", reqUri, reqType);
}


These filters are the only code added by me that interacts with the request and reponse between the applications and the end point. I've also tried to remove these filters, without any luck. Removing the responseFilter breaks the communication with the AngularJS app, while removing the requestFilter only stops the logging.

@POST
@Consumes({MULTIPART_FORM_DATA})
public Response createPicture(MultipartFormDataInput input) {

for (InputPart inputPart : input.getParts()) {
try {
fileController.saveFile(inputPart);

return Response.ok().build();
} catch (FileNotSavedException e) {
return Response.serverError(e.getMessage()).build();
}
}

return badRequestNullResponse();
}


Code for uploading the picture (Android):

public static void uploadBitmap(Bitmap bitmap, String filename)
throws IOException {
URL url = new URL(URL_REST_API + FILE);
HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
httpUrlConnection.setUseCaches(false);
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setDoInput(true);

httpUrlConnection.setRequestMethod("POST");
httpUrlConnection.setRequestProperty("Connection", "Keep-Alive");
httpUrlConnection.setRequestProperty("Cache-Control", "no-cache");
httpUrlConnection.setRequestProperty("Accept", "multipart/form-data");
httpUrlConnection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY);

// EDIT 2: This following log statement was omitted in the first post.
// I extracted the setRequestProperty statements from another method
// due readability of this question, but I had missed to copy this:
Log.e(LOG_TAG, "Headers: \n" + httpUrlConnection.getHeaderFields());

DataOutputStream request = new DataOutputStream(httpUrlConnection.getOutputStream());

// Part [start]
request.writeBytes(DOUBLE_HYPHEN + BOUNDARY + CR_LF);
request.writeBytes("Content-Disposition: form-data;filename=\"" + filename + "\"" + CR_LF);
request.writeBytes(CR_LF);

// Part [content]
bitmap.compress(Bitmap.CompressFormat.JPEG, IMAGE_QUALITY_PERCENTAGE, request);

// part [end]
request.writeBytes(CR_LF);
request.writeBytes(DOUBLE_HYPHEN + BOUNDARY + DOUBLE_HYPHEN + CR_LF);

request.flush();
request.close();

httpUrlConnection.disconnect();
}


As mentioned I'm starting "each" part with double hyphens, the boundary and a line break. Since the headers are created by
setRequestProperty
I would assume the header are ended correctly. So is it caused by a missing body, in that case; why isn't the
Content-Disposition
or the picture file written to the request?




07:55:28,479 WARN [org.apache.james.mime4j.parser.MimeEntity] (default task-36) Unexpected end of headers detected. Higher level boundary detected or EOF reached.
07:55:28,479 WARN [org.apache.james.mime4j.parser.MimeEntity] (default task-36) Invalid header encountered
07:55:28,479 WARN [org.apache.james.mime4j.parser.MimeEntity] (default task-36) Body part ended prematurely. Boundary detected in header or EOF reached.
07:55:28,479 WARN [org.jboss.resteasy.core.ExceptionHandler] (default task-36) Failed executing POST /file: org.jboss.resteasy.spi.ReaderException: java.lang.RuntimeException: Could find no Content-Disposition header within part
at org.jboss.resteasy.core.MessageBodyParameterInjector.inject(MessageBodyParameterInjector.java:183)
at org.jboss.resteasy.core.MethodInjectorImpl.injectArguments(MethodInjectorImpl.java:89)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:112)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:296)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:250)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:237)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:356)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:179)
at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:220)
at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:86)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:58)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:72)
at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
at io.undertow.security.handlers.SecurityInitialHandler.handleRequest(SecurityInitialHandler.java:76)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:282)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:261)
at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:80)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:172)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:199)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:774)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException: Could find no Content-Disposition header within part
at org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInputImpl.extractPart(MultipartFormDataInputImpl.java:68)
at org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl.extractParts(MultipartInputImpl.java:229)
at org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl.parse(MultipartInputImpl.java:198)
at org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataReader.readFrom(MultipartFormDataReader.java:52)
at org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataReader.readFrom(MultipartFormDataReader.java:20)
at org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataReader$Proxy$_$$_WeldClientProxy.readFrom(Unknown Source)
at org.jboss.resteasy.core.interception.AbstractReaderInterceptorContext.readFrom(AbstractReaderInterceptorContext.java:59)
at org.jboss.resteasy.core.interception.ServerReaderInterceptorContext.readFrom(ServerReaderInterceptorContext.java:62)
at org.jboss.resteasy.core.interception.AbstractReaderInterceptorContext.proceed(AbstractReaderInterceptorContext.java:51)
at org.jboss.resteasy.security.doseta.DigitalVerificationInterceptor.aroundReadFrom(DigitalVerificationInterceptor.java:32)
at org.jboss.resteasy.core.interception.AbstractReaderInterceptorContext.proceed(AbstractReaderInterceptorContext.java:53)
at org.jboss.resteasy.plugins.interceptors.encoding.GZIPDecodingInterceptor.aroundReadFrom(GZIPDecodingInterceptor.java:59)
at org.jboss.resteasy.plugins.interceptors.encoding.GZIPDecodingInterceptor$Proxy$_$$_WeldClientProxy.aroundReadFrom(Unknown Source)
at org.jboss.resteasy.core.interception.AbstractReaderInterceptorContext.proceed(AbstractReaderInterceptorContext.java:53)
at org.jboss.resteasy.core.MessageBodyParameterInjector.inject(MessageBodyParameterInjector.java:150)
... 38 more


Also; I found some WARN messages regarding
WFLYWELD0052: (...) Package-private access will not work.
So I fixed these by adding
core
and
spi
as dependencies in the warned modules, as suggested in this issue. It removed the warning messages, but did not fix the problem.

EDIT:



Here are the logs from an upload request by the android application, where I'm printing the headers sent with the request:

(default task-42) Header: [Accept]
(default task-42) Value: [multipart/form-data]
(default task-42) Header: [Cache-Control]
(default task-42) Value: [no-cache]
(default task-42) Header: [Connection]
(default task-42) Value: [Keep-Alive]
(default task-42) Header: [User-Agent]
(default task-42) Value: [Dalvik/2.1.0 (Linux; U; Android 5.0.2; SM-A500FU Build/LRX22G)]
(default task-42) Header: [Host]
(default task-42) Value: [***.**.***.***:8080]
(default task-42) Header: [Accept-Encoding]
(default task-42) Value: [gzip]
(default task-42) Header: [Content-Length]
(default task-42) Value: [0]
(default task-42) Header: [Content-Type]
(default task-42) Value: [multipart/form-data;boundary=*****]
(default task-42) :: Source :: [(*.***.**.**) null@*.***.**.**:28686]
(default task-42) :: User :: [unknown]
(default task-42) :: Boundary :: [/upload/api/file/] - [POST]
(default task-42) Unexpected end of headers detected. Higher level boundary detected or EOF reached.
(default task-42) Invalid header encountered
(default task-42) Body part ended prematurely. Boundary detected in header or EOF reached.


When uploading through Postman, I'm able to access the end point, as expected - without adding more than the file (perhaps Postman creates the content-disposition tags automatically?).

EDIT 2:



I have updated the Android code with the reason of error (see
Edit 2
-comment). It was actually a log statement that caused the incorrect behaviour. I'll add an answer to explain why!

Answer

Reason of failure:

The reason this happened was caused by a log statement I added, to verify the headers sent to the server. In my original question this was omitted, since the following was originally extracted from another method. When I copied the snippet out from this method I didn't think of the log statement as related to the problem.

httpUrlConnection.setUseCaches(false);
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setDoInput(true);

httpUrlConnection.setRequestMethod("POST");
httpUrlConnection.setRequestProperty("Connection", "Keep-Alive");
httpUrlConnection.setRequestProperty("Cache-Control", "no-cache");
httpUrlConnection.setRequestProperty("Accept", "multipart/form-data");
httpUrlConnection.setRequestProperty("Content-Type", 
    "multipart/form-data;boundary=" + BOUNDARY);

The following line were also in the extracted method, causing the problem.

Log.e(LOG_TAG, "Headers: \n" + httpUrlConnection.getHeaderFields());

Explaination:

Why didn't you notice this before?

The method uploadBitmap just throws IOException and the caller method just swallowed the exception and printed out "Could not upload picture; server responded with 406". This bad exception handling made me focus too much on thinking the server caused the issue.

What actually happened?

The exception message that I missed in the Android application was cannot write request body after response has been read, which made me look through each line of the code, till I found that the log statement tried to read the headers.

Why did this cause error?

I'm not 100% sure about the actual reason for this, but I can imagine that reading from the connection tells the server;

"I'm done with what ever I want to say to you, now I want to know what you have to say to me".

So when I read from the connection after writing the headers, it seems that I'm basically sending the request to get the response. This then caused the error where the server couldn't find the Content-Disposition, since there never were any Content-Dispositions, parts or even a body for the server to process.

Once again; I'm not 100% sure about this, since I havn't looked it up yet.

Conclusion:

Well, after too many hours wasted on this error, I discovered that I should be more aware of my log statements. I should also stop using log statements to get values I just as easily can get by simple, plain debugging.