Here is the scenario.
I am using Play framework. Inside a given handler, the play framework calls my API webservice and returns the API response to the client. The client is calling the handler through an Ajax call. Sometimes the response comes fine but often i am seeing error response on the client side. Checking the logs of play framework, i see a java.nio.channels.ClosedChannelException.
I am using Play 2.1.1.
My API Webservice is running on localhost:8888. Play framework is running on 9000.
The API service response is correct. Play frameworks also executes the Callback correctly as i can see the logs. The error happens after the ok() call has been made from Play.
Here are the error logs for a failed request -
[debug] application - find...
[debug] application - id = 647110558
[trace] c.jolbox.bonecp - Check out connection [9 leased]
[trace] c.jolbox.bonecp - Check in connection [9 leased]
[debug] application - socialUser = SocialUser(UserId(647110558,facebook),Arvind,Batra,Arvind Batra,Some(arvindbatra#gmail.com),null,AuthenticationMethod(oauth2),null,Some(OAuth2Info(CAAHNVOUuNZAEBAMa3CPLUEsZA2Tp5xWGXylO9HggBY0TCfwsIn4iGUdlRMpuNPLxYcObKO5ZBZCU0ghS9ymHZC3s9YXpsfPix9AM1EhNyETvDR85HHYg8j7JO0h2WzGZBsKJdbFPhPmkD6ZBZAq6KTT8RLSQrmpfnHQZD,null,null,null)),null)
[info] application - Calling interest for fff
[info] application - user is not null
[trace] c.jolbox.bonecp - Check out connection [9 leased]
[info] application - interest=fff, userInfo:models.EBUser#14a420e1
[info] application - http://localhost:8888/api/add_interest/1/fff
[debug] c.n.h.c.p.n.NettyAsyncHttpProvider - Using cached Channel [id: 0x9d1dee2d, /127.0.0.1:50316 => localhost/127.0.0.1:8888]
for uri http://localhost:8888/api/add_interest/1/fff
[debug] c.n.h.c.p.n.NettyAsyncHttpProvider -
Using cached Channel [id: 0x9d1dee2d, /127.0.0.1:50316 => localhost/127.0.0.1:8888]
for request
DefaultHttpRequest(chunked: false)
GET /api/add_interest/1/fff HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Accept: */* User-Agent: NING/1.0
[debug] c.n.h.c.p.n.NettyAsyncHttpProvider -
Request DefaultHttpRequest(chunked: false)
GET /api/add_interest/1/fff HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Accept: */*
User-Agent: NING/1.0
Response DefaultHttpResponse(chunked: true)
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Thu, 04 Jul 2013 12:14:40 GMT
Transfer-Encoding: chunked
[debug] c.n.h.c.p.n.NettyConnectionsPool - Adding uri: http://localhost:8888 for channel [id: 0x9d1dee2d, /127.0.0.1:50316 => localhost/127.0.0.1:8888]
[info] application - {"status":"success"}
[info] application - {"status":"ok","exists":false}
[trace] play - Sending simple result: SimpleResult(200, Map(Content-Type -> application/json; charset=utf-8, Set-Cookie -> ))
[debug] play - java.nio.channels.ClosedChannelException
[trace] application - Exception caught in Netty
java.nio.channels.ClosedChannelException: null
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.cleanUpWriteBuffer(AbstractNioWorker.java:409) ~[netty.jar:na]
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.writeFromUserCode(AbstractNioWorker.java:127) ~[netty.jar:na]
at org.jboss.netty.channel.socket.nio.NioServerSocketPipelineSink.handleAcceptedSocket(NioServerSocketPipelineSink.java:99) ~[netty.jar:na]
at org.jboss.netty.channel.socket.nio.NioServerSocketPipelineSink.eventSunk(NioServerSocketPipelineSink.java:36) ~[netty.jar:na]
at org.jboss.netty.channel.Channels.write(Channels.java:725) ~[netty.jar:na]
at org.jboss.netty.handler.codec.oneone.OneToOneEncoder.doEncode(OneToOneEncoder.java:71) ~[netty.jar:na]
[debug] c.n.h.c.p.n.NettyConnectionsPool - Entry count for : http://localhost:8888 : 2
Here is my sample code -
public static Result addInterestCallback(WS.Response response) {
if (response == null) {
return badRequest();
}
ObjectNode result = (ObjectNode) response.asJson();
try {
Logger.info(result.toString());
if (result.has("status")) {
String status = result.get("status").getTextValue();
if(status.equals("error")) {
result.put("error", "Oops, cannot process your request. Sorry.");
Logger.info("error");
return badRequest(result);
}
else if(status.equals("exists")) {
result.put("exists",true);
}
else {
result.put("exists",false);
}
result.put("status", "ok");
} else {
//do something
Logger.info("result has no status");
}
Logger.info(result.toString());
} catch (Exception e) {
e.printStackTrace();
}
return ok(result);
}
#BodyParser.Of(BodyParser.Json.class)
#SecureSocial.UserAwareAction
public static Result addInterest() {
JsonNode json = request().body().asJson();
String interestName = json.findPath("interestName").getTextValue();
Logger.info("Calling interest for " + interestName);
Identity user = (Identity) ctx().args.get(SecureSocial.USER_KEY);
if (user == null) {
ObjectNode result = Json.newObject();
result.put("error", "requires-login");
Http.Context ctx = Http.Context.current();
ctx.flash().put("error", play.i18n.Messages.get("securesocial.loginRequired"));
result.put("redirect", RoutesHelper.login().absoluteURL(ctx.request(), IdentityProvider.sslEnabled()));
return ok(result);
}
Logger.info("user is not null");
if(interestName == null) {
ObjectNode result = Json.newObject();
result.put("error", "Empty input");
return badRequest(result);
}
//get user
EBUser ebUser = Application.getEBUser();
Logger.info("interest="+interestName+", userInfo:" + ebUser.toString());
//Call addInterst API.
String apiEndpoint = Play.application().configuration().getString(AppConstants.EB_API_ENDPOINT);
String url = "";
try {
url = apiEndpoint + "add_interest/" + ebUser.getId() + "/" + URLEncoder.encode(interestName, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
ObjectNode result = Json.newObject();
result.put("error", "Cant parse interest properly");
Logger.info("error " + result.toString());
return badRequest(result);
}
Logger.info(url);
Promise<WS.Response> promiseOfAPI = WS.url(url).get();
Promise<Result> promiseOfResult = promiseOfAPI.map(
new Function<WS.Response, Result>() {
#Override
public Result apply(WS.Response response) throws Throwable {
return addInterestCallback(response);
}
});
return async(promiseOfResult);
}
Name of Handler is addInterest.
Any pointers on what could be happening here?
java.nio.channels.ClosedChannelException
This means you have closed the channel and then continued to use it.
Related
I went through the other similar questions but couldn't find any solution. We have a backend service running by Spring Boot and has been working for a while now. But there is a new user of this service recently and they are using MuleSoft to send their request. But all attempts to send a file to our service fails with this error:
Failed to parse multipart servlet request; nested exception is java.io.IOException: org.apache.tomcat.util.http.fileupload.FileUploadException: the request was rejected because no multipart boundary was found"
The only difference we could find between a request from MuleSoft and say a curl command is that MuleSoft always sends the request with boundary value wrapped with double quotes
Mule request:
<Header name="Content-Type">multipart/form-data; charset=UTF-8; boundary="--------------------------669816398596264398718285"</Header>
Versus Postman/curl request:
* SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fd656810a00)
> POST /api/upload HTTP/2
> Host: myhost
> user-agent: curl/7.79.1
> accept: */*
> content-length: 97255
> content-type: multipart/form-data; boundary=------------------------111fd08cb4fafe1c
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
* We are completely uploaded and fine
< HTTP/2 200
< date: Mon, 19 Dec 2022 04:56:25 GMT
< content-length: 0
Our controller in Spring is very simple:
#RestController
class MyController {
#PostMapping("/upload", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
#ResponseStatus(HttpStatus.OK)
fun uploadDocument(#RequestPart("file") file: MultipartFile) {
logger.info { "ContentType: ${file.contentType}" }
logger.info { "Name: ${file.name}" }
logger.info { "Byte: ${String(file.bytes)}" }
}
}
The following curl command works fine:
curl -v -X POST -F file=#/Users/myhomefolder/Documents/some-file.jpg https://host-name/api/upload
But this script from MuleSoft doesn't (Sorry I'm not familiar with Mule, I got this code from their team):
import dw::module::Multipart
output multipart/form-data boundary = "---WebKitFormBoundary7MA4YWxkTrZu0gW"
---
{
parts : {
file : {
headers : {
"Content-Disposition" : {
"name": "file",
"filename": payload.name
},
"Content-Type" : "multipart/form-data"
},
content : payload.byteArray
}
}
}
Is there any configuration in Spring that accepts double quotes for boundary? Is there anything missing in our backend configuration that should be added to support different HTTP client?
We are sending mails in an email service with org.springframework.mail.javamail.JavaMailSender via an office 365 account and SMTP and set the following parameters in application.yml:
spring:
mail:
host: ${EMAIL_HOST:smtp.office365.com}
port: ${EMAIL_PORT:587}
username: ${EMAIL_USERNAME}
password: ${EMAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
starttls:
enable: true
socketFactory:
port: 465
class: javax.net.ssl.SSLSocketFactory
The strange thing is: if we set the connectiontimeout to 5s, the service gets a response after 5s. If we set it to 20s, the o365 responds after 20s.
My expectation is that <connectiontimeout> is the maximum amount of time, that the sending may take and not the actual time.
Funny thing is that when setting another provider than office365, connectiontimeout works as expected.
Does anyone have this issue as well and maybe know how to solve that?
Our sender service:
#PostMapping
#ResponseStatus(HttpStatus.ACCEPTED)
public void sendMail(#RequestHeader(name = "X-API-KEY", required = true) String requestApiKey, #Valid #RequestBody EmailSendRequest email, HttpServletResponse response) {
if(!apiKey.equals(requestApiKey)){
LOGGER.error("Unauthorized api key" + requestApiKey);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
try {
LOGGER.info("Received request to send mail Subject=[{}] To=[{}] From=[{}]", email.getSubject(), email.getTo(), email.getFrom());
MimeMessage message = mailSender.createMimeMessage();
message.setFrom(new InternetAddress(email.getFrom().getEmail()));
message.addRecipients(Message.RecipientType.TO, toAddressArray(email.getTo()));
message.addRecipients(Message.RecipientType.CC, toAddressArray(email.getCc()));
message.addRecipients(Message.RecipientType.BCC, toAddressArray(email.getBcc()));
message.setSubject(email.getSubject());
message.setSentDate(new Date());
Multipart multipart = new MimeMultipart();
MimeBodyPart messageText = new MimeBodyPart();
messageText.setContent(email.getContent().getValue(),
email.getContent().getType() == null ? DEFAULT_CONTENT_MIMETYPE : email.getContent().getType());
multipart.addBodyPart(messageText);
addAttachments(multipart, email.getAttachments());
message.setContent(multipart);
if(message.getRecipients(Message.RecipientType.TO) != null ||
message.getRecipients(Message.RecipientType.CC) != null ||
message.getRecipients(Message.RecipientType.BCC) != null)
{
mailSender.send(message);
}
else {
LOGGER.warn("Email not send! No recipients or all ignored.");
response.setHeader("X-Ignored","true");
}
LOGGER.info("Mail Subject=[{}] To=[{}}] From=[{}] successfully sent.",email.getSubject(),email.getTo(),email.getFrom());
} catch (MessagingException e) {
LOGGER.error("Error sending mail Subject=[{}] To=[{}] From=[{}]:", email.getSubject(), email.getTo(), email.getFrom(), e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
} catch (MailSendException mailSendException) {
Exception[] exceptions = mailSendException.getMessageExceptions();
for (Exception e : exceptions){
if (e instanceof SMTPSendFailedException && (((SMTPSendFailedException)e).getReturnCode() == 554)){
LOGGER.error("Error sending mail Subject=[{}] To=[{}] From=[{}]: This sender mail address is not allowed.", email.getSubject(), email.getTo(), email.getFrom());
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
}
LOGGER.error("Error sending mail Subject=[{}] To=[{}] From=[{}]:", email.getSubject(), email.getTo(), email.getFrom(), mailSendException);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
} catch (MailAuthenticationException e) {
LOGGER.error("Error sending mail Subject=[{}] To=[{}] From=[{}]: Wrong SMTP login credentials provided. \nMSG:{}", email.getSubject(), email.getTo(), email.getFrom(),e.getMessage());
throw new ResponseStatusException(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED);
}
}
It seems, that the SocketFactory was responsible for this behaviour. Removing the following lines from application.yml makes the application work as expected:
socketFactory:
port: 465
class: javax.net.ssl.SSLSocketFactory
I have a Kotlin #RestController, and I would expect it to return 400 Bad Request in a situation where a null is passed for a #RequestParam argument.
Example:
#RestController
class Api() {
#PostMapping("endpoint")
fun endpoint(#DateTimeFormat(iso = DATE) #RequestParam date: LocalDate) {
//do something
}
}
If I were to make a request to POST /endpoint?date I get a 500 Internal Server Error with the following (shortened) body:
{
"timestamp": "2020-09-14T20:39:38.102+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Parameter specified as non-null is null: method Api.endpoint, parameter date",
"trace": "java.lang.IllegalArgumentException: Parameter specified as non-null is null: method Api.endpoint, parameter date\r\n\tat Api.endpoint(Api.kt)\r\n\t
...
...
atjava.base/java.lang.Thread.run(Thread.java:834)\r\n",
"path": "/campaigns/contributions/unfunded/retries"
}
Is there any way to fix this using either some additional library, a configuration, or some custom code that does not have other side effects that keeps everything the same except that the status code will be 400 Bad Request
The following works for Kotlin 1.4.*. This is not necessarily the best answer because there is no guarantee that Kotlin will not change the excption type of message in future releases. For eaxample, when I originally asked the question I was using 1.3.*, and the exception was IllegalArgumentException. In 1.4.* it has been changed to NullPointerException.
#ControllerAdvice
class KotlinNonNullParamHandling {
#ExceptionHandler
protected fun handleKotlinNonNullViolation(
exception: NullPointerException,
response: HttpServletResponse
) {
val nullParameter = exception.message?.startsWith("Parameter specified as non-null is null: ") == true
val restController = exception
.stackTrace
.getOrNull(0)
?.let { Class.forName(it.className) }
?.getAnnotation(RestController::class.java) != null
val status =
if (nullParameter && restController) HttpStatus.BAD_REQUEST
else HttpStatus.INTERNAL_SERVER_ERROR
response.sendError(status.value(), exception.message)
}
}
Before assuming that it is 400, we are checking that the exception
is a NullPointerException
has a message starting with "Parameter specified as non-null is null: "
it was thrown from a class the is RestController (otherwise it might be some other problem, such as a null being sent to a method by reflection having nothing to do with the web layer.)
Spring will throw the IllegalArgumentException by default when a validation fails here. Every exception inside your Spring-Boot application will be treated by default as InternalServerError.
You can modify your response code by adding a function like this:
#RestController
class Api() {
#ExceptionHandler(IllegalArgumentException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public void onIllegalArgumentException(IllegalArgumentException e) {
}
#PostMapping("endpoint")
fun endpoint(#DateTimeFormat(iso = DATE) #RequestParam date: LocalDate) {
//do something
}
}
Afterwards you'll get the 400 status code on invalid request param.
I've tested your code, and there's nothing obviously wrong with it - works as expected.
curl -v -d "date=2020-10-01" http://localhost:8080/endpoint
* Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /endpoint HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 15
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 15 out of 15 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Length: 0
< Date: Tue, 13 Oct 2020 07:04:21 GMT
<
* Connection #0 to host localhost left intact
So I would:
Verify that the client is properly formatting the request - e.g. try explicitly setting the Content-Type header to application/x-www-form-urlencoded, check if there's no typo in the param name etc.
That you have Jackson properly configured to work with Kotlin (com.fasterxml.jackson.module:jackson-module-kotlin - dependency)
I'm using spring boot validation to validate the #PathVariable as shown in the code below, the validation works however there is no message returned when I execute through curl.exe, I expect to receive the default message.
#GetMapping("/number/{id}")
String getNumber(#PathVariable #Min(2) Long id) {
System.out.println("getNumber :" + id);
return "param id is : " + id;
}
I have specified a CustomGlobalExceptionHandler to catch ConstraintViolationException and this works in that the returned http status code is 400 and it displays a message in the console log "must be greater than or equal to 2"
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
void onConstraintValidationException(ConstraintViolationException e) {
System.out.println("in onConstraintValidationException - start");
String error = "blank";
for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
error = violation.getMessage();
System.out.println(error);
}
System.out.println("in onConstraintValidationException - end");
}
I'm using the command line to execute the REST Service
curl.exe -v -X GET "http://localhost:8080/demo-0.0.2-SNAPSHOT/number/1" -H "accept: /"
and I receive this output, but no message
*Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /demo-0.0.2-SNAPSHOT/number/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.55.1
> accept: */*
>
< HTTP/1.1 400
< Transfer-Encoding: chunked
< Date: Tue, 11 Aug 2020 09:28:06 GMT
< Connection: close
<
* Closing connection 0*
I based this on the mykong.com example https://mkyong.com/spring-boot/spring-rest-validation-example/ section 3. Path Variables Validation. where the results should match
curl -v localhost:8080/books/0
{
"timestamp":"2019-02-20T13:35:59.808+0000",
"status":400,
"error":"Bad Request",
"message":"findOne.id: must be greater than or equal to 1",
"path":"/books/0"
}
Can you suggest why I don't receive a message returned back to the command line?
Did you add these annotations to your controller:
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min;
#RestController
#Validated
You have to implement this class in order to get 400 message instead of 500:
#ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
//..
}
Server Side Code
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
try
{
string clientId;
string clientSecret, deviceId;
string appId = context.Parameters["applicationId"];//renamed from applicationName
string version = context.Parameters["version"];
deviceId = context.Parameters["deviceId"];//renamed from applicationName
context.OwinContext.Set("appId", appId);
ApiAppClient client;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (string.IsNullOrWhiteSpace(clientId))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (string.IsNullOrWhiteSpace(clientId))
{
var query = context.Parameters.Where(x => x.Key == "client_id");
if (query.Any())
{
clientId = query.FirstOrDefault().Value?[0];
}
}
if (string.IsNullOrWhiteSpace(clientId))
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
//context.Validated();
context.SetError($"Missing ClientId.");
////context.Rejected();
return;
}
Client Side Code
POST https://example.com/token HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Host: example.com
Content-Length: 258
client_id=xxxxxxxxxxxxxxxxxxxxxx&client_secret=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz&applicationId=aaaaaaaaaaaaaaaaaaaaaaaa&username=me&password=somthing&grant_type=password&deviceId=5545555&version=3.2.1.89
I have a strange case for above OAuth process ClientId is null as well as Parameters is empty.
The above code works fine on all of our developer machines.
Compiled Version of Web API also works if hosted in IIS on Developer Machine.
But When I try to host Web API on my remote server it always gives error "Missing ClientId".
It uses to work perfectly on the same server before upgrading NuGet packages.
Microsoft.Owin.Security.OAuth 3.0.1.0 upgraded to 3.1.0.0
Microsoft.Owin.Security 3.0.1.0 upgraded to 3.1.0.0
And All Unity Packages