How do I properly close a websocket and and provide a clean, informative response to the client when an internal error occurs on my server? In my current case, the client must provide a parameter when it connects, and I am trying to handle incorrect or missing parameters received by OnOpen.
This example suggests I can just throw an exception in OnOpen, which will ultimately call OnError where I can close with a reason and message. It kinda works, but the client only receives an EOF, 1006, CLOSE_ABNORMAL.
Also, because I have found no other discussion, I can't tell what might be best practice.
I'm using the JSR-356 spec, as follows:
#ClientEndpoint
#ServerEndpoint(value="/ws/events/")
public class WebSocketEvents
{
private javax.websocket.Session session;
private long token;
#OnOpen
public void onWebSocketConnect(javax.websocket.Session session) throws BadRequestException
{
logger.info("WebSocket connection attempt: " + session);
this.session = session;
// this throws BadRequestException if null or invalid long
// with short detail message, e.g., "Missing parameter: token"
token = HTTP.getRequiredLongParameter(session, "token");
}
#OnMessage
public void onWebSocketText(String message)
{
logger.info("Received text message: " + message);
}
#OnClose
public void onWebSocketClose(CloseReason reason)
{
logger.info("WebSocket Closed: " + reason);
}
#OnError
public void onWebSocketError(Throwable t)
{
logger.info("WebSocket Error: ");
logger.debug(t, t);
if (!session.isOpen())
{
logger.info("Throwable in closed websocket:" + t, t);
return;
}
CloseCode reason = t instanceof BadRequestException ? CloseReason.CloseCodes.PROTOCOL_ERROR : CloseReason.CloseCodes.UNEXPECTED_CONDITION;
try
{
session.close(new CloseReason(reason, t.getMessage()));
}
catch (IOException e)
{
logger.warn(e, e);
}
}
}
Edit: The exception throwing per linked example seems weird, so now I am catching exception within OnOpen and immediately doing
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "some text"));
Edit: This turned out to be correct, though a separate bug disguised it for a while.
Edit2: Clarification: HTTP is my own static utility class. HTTP.getRequiredLongParameter() gets query parameters from the client's initial request by using
session.getRequestParameterMap().get(name)
and does further processing.
To develop the points I mentioned, your problem about "how to handle a required parameter", I can see following options. First of all, let's consider the endpoint:
#ServerEndpoint(value = "/websocket/myendpoint",
configuration = MyWebsocketConfiguration.class)
public class MyEndpoint{
// #OnOpen, #OnClose, #OnMessage, #OnError...
}
Filtering
The first contact between a client and a server is a HTTP request. You can filter it with a filter to prevent the websocket handshake from happening. A filter can either block a request or let it through:
import javax.servlet.Filter;
public class MyEndpointFilter implements Filter{
#Override
public void init(FilterConfig filterConfig) throws ServletException {
// nothing for this example
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// if the connection URL is /websocket/myendpoint?parameter=value
// feel free to dig in what you can get from ServletRequest
String myToken = request.getParameter("token");
// if the parameter is mandatory
if (myToken == null){
// you can return an HTTP error code like:
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// if the parameter must match an expected value
if (!isValid(myToken)){
// process the error like above, you can
// use the 403 HTTP status code for instance
return;
}
// this part is very important: the filter allows
// the request to keep going: all green and good to go!
chain.doFilter(request, response);
}
#Override
public void destroy() {
//nothing for this example
}
private boolean isValid(String token){
// how your token is checked? put it here
}
}
If you are using a filter, you must add it in your web.xml:
<web-app ...>
<!-- you declare the filter here -->
<filter>
<filter-name>myWebsocketFilter</filter-name>
<filter-class>com.mypackage.MyEndpointFilter </filter-class>
<async-supported>true</async-supported>
</filter>
<!-- then you map your filter to an url pattern. In websocket
case, it must match the serverendpoint value -->
<filter-mapping>
<filter-name>myWebsocketFilter</filter-name>
<url-pattern>/websocket/myendpoint</url-pattern>
</filter-mapping>
</web-app>
the async-supported was suggested by BalusC in my question to support asynchronous message sending.
TL,DR
if you need to manipulate GET parameters provided by the client at the connection time, Filter can be a solution if you are satisfied with a pure HTTP answer (403 status code and so on)
Configurator
As you may have noticed, I have added configuration = MyWebsocketConfiguration.class. Such class looks like:
public class MyWebsocketConfigurationextends ServerEndpointConfig.Configurator {
// as the name suggests, we operate here at the handshake level
// so we can start talking in websocket vocabulary
#Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// much like ServletRequest, the HandshakeRequest contains
// all the information provided by the client at connection time
// a common usage is:
Map<String, List<String>> parameters = request.getParameterMap();
// this is not a Map<String, String> to handle situation like
// URL = /websocket/myendpoint?token=value1&token=value2
// then the key "token" is bound to the list {"value1", "value2"}
sec.getUserProperties().put("myFetchedToken", parameters.get("token"));
}
}
Okay, great, how is this different from a filter? The big difference is that you're adding here some information in the user properties during the handshake. That means that the #OnOpen can have access to this information:
#ServerEndpoint(value = "/websocket/myendpoint",
configuration = MyWebsocketConfiguration.class)
public class MyEndpoint{
// you can fetch the information added during the
// handshake via the EndpointConfig
#OnOpen
public void onOpen(Session session, EndpointConfig config){
List<String> token = (List<String>) config.getUserProperties().get("myFetchedToken");
// now you can manipulate the token:
if(token.isEmpty()){
// for example:
session.close(new CloseReasons(CloseReason.CloseCodes.CANNOT_ACCEPT, "the token is mandatory!");
}
}
// #OnClose, #OnMessage, #OnError...
}
TL;DR
You want to manipulate some parameters but process the possible error in a websocket way? Create your own configuration.
Try/catch
I also mentioned the try/catch option:
#ServerEndpoint(value = "/websocket/myendpoint")
public class MyEndpoint{
#OnOpen
public void onOpen(Session session, EndpointConfig config){
// by catching the exception and handling yourself
// here, the #OnError will never be called.
try{
Long token = HTTP.getRequiredLongParameter(session, "token");
// process your token
}
catch(BadRequestException e){
// as you suggested:
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "some text"));
}
}
// #OnClose, #OnMessage, #OnError...
}
Hope this help
I believe I should have placed...
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "some text"));
...right where the error occurs, within #OnOpen(). (For generic errors, use CloseCodes.UNEXPECTED_CONDITION.)
The client receives:
onClose(1003, some text)
This is, of course, the obvious answer. I think I was misled, by the example cited, into throwing an exception from #OnOpen(). As Remy Lebeau suggested, the socket was probably closed by this, blocking any further handling by me in #OnError(). (Some other bug may have obscured the evidence that was discussed.)
Related
tl;dr:
How do I get the ServletResponse during ServletRequestListener.requestDestroyed?
Short Version
In JavaEE, I want to know when:
when a request starts
and when a request ends
and be able to inspect the request and response objects.
Long Version
In the ASP.NET world, if you want to know when a request starts and ends, you write an IHttpModule:
public class ExampleModuleForThisQuestion : IHttpModule
{
}
And then register your "module" in the web XML configuration file:
web.config:
<system.webServer>
<modules>
<add name="DoesntMatter" type="ExampleModuleForThisQuestion "/>
</modules>
</system.webServer>
Inside your module, you can register callback handlers for:
BeginRequest event
EndRequest event
The web server infrastructure then calls you Init method. That is your opportunity to register that you want to receive notifications when a request starts, and when a request ends:
public class ExampleModuleForThisQuestion : IHttpModule
{
public void Init(HttpApplication application)
{
application.BeginRequest += new EventHandler(beginRequest); //register the "BeginRequet" event
application.EndRequest += new EventHandler(endRequest); //register the "EndRequest" event
}
}
And now we have our callbacks when a request starts:
private void beginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
//Record the time the request started
application.Context.Items["RequestStartTime"] = DateTime.Now;
//We can even access the Request and Response objects
application.ContenxtLog(application.Context.Request.Headers["User-Agent"]);
}
And we have our callback when a request ends:
private void endRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
//We can even access the Request and Response objects
//Get the response status code (e.g. 418 I'm a teapot)
int statusCode = application.Context.Response.StatusCode;
//Get the request method (e.g. GET, POST, BREW)
String method = application.context.Request.RequestType;
//Get the path from the request (e.g. /ViewCustomer)
String path = application.context.Request.AppRelativeCurrentExecutionFilePath'
//Get when the request started - that we recorded during "Begin Request"
DateTime requestStartTime = (DateTime)application.Context.Items["RequestStartTime"];
//And we can modify the response
if ((DateTime.Now - requestStartTime).TotalSeconds = 17)
application.Context.Response.StatusCode = 451;
}
The Java Almost-Equivalent is ServletRequestListener
In Java, apparently the corresponding technique is to create and object that implements the ServletRequestListener interface:
#WebListener
public class ExampleListenerForThisQuestion
implements javax.servlet.ServletRequestListener {
}
and register our listener with the application server by including it in our web XML configuration file:
web.xml
<listener>
<listener-class>ExampleListenerForThisQuestion</listener-class>
</listener>
Now we can implement the requestInitialized and requestDestroyed methods to get when a request starts and ends:
public class ExampleListenerForThisQuestion
implements javax.servlet.ServletRequestListener {
#Override
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest sr = sre.getServletRequest();
sr.setAttribute("requestStartTicks", getCurrentTickCount());
HttpServletRequest request = (HttpServletRequest) sr;
// e.g. "PUT /Customers/1234"
System.out.printf("%s %s\r\n", request.getMethod());
}
#Override
public void requestDestroyed(ServletRequestEvent sre) {
ServletRequest sr = sre.getServletRequest();
long requestStartTicks = (long)sr.getAttribute("requestStartTicks");
HttpServletResponse response = (HttpServletRequest)...nothing, because i don't know how...
// e.g. "226 IM Used"
System.out.printf("%d %s\r\n", response.getStatus(), response.getStatusDescription());
}
}
But how do we get the response?
Now that I'm notified when the response ends, I need the result of that request:
I need the HTTP status code (e.g., 424)
I need the HTTP status description (e.g., Failed Dependency)
I need to inspect response headers
I need to modify response headers
You notice the line in my code above:
HttpServletResponse response = (HttpServletRequest)...nothing, because i don't know how...
How can I get hold of the response?
You can create a Filter instead of a listener.
Filters allow you to create wrappers around request processing. See the documentation on that topic.
For HTTP, you can use HTTPFilter. This could look like the following:
#WebFilter("/*")//or via deployment descriptor
public class YourFilter extends HttpFilter{ //or just Filter for general (non-HTTP) processing
#Override
public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {//for generic filters, use ServletRequest/ServletResponse instead
//before request processing
chain.doFilter(req, res);//calls other filters and processes request
//after request processing
//you can use res here
}
}
If you do not call chain.doFilter, other filters and the servlet will not be executed.
If you prefer declaring the filter in your deployment descriptor (web.xml), you can do that as well:
<filter>
<filter-name>yourFilter</filter-name>
<filter-class>your.FilterClass</filter-class>
</filter>
<filter-mapping>
<filter-name>yourFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
I'm having a problem on HttpAsyncRequestExecutor.
I'm using elasticsearch Java Rest Client and I'm always getting a ConnectionClosedException (see below) when I call the performRequestAsync:
// variables (all with valid format):
// endpoint is just a List<String> with "14655/_search"
// params is just a Map<String, String> with
// "pretty", "true"
// "search_type", "query_then_fetch"
// entity is just a HttpEntity entity with the Json body request
final int numRequests = endpoints.size();
final CountDownLatch latch = new CountDownLatch(numRequests);
try (Timer.Context ctx = this.requestTimer.time()) {
for (final String endpoint : endpoints) {
// ERROR hapens here:
restClient.performRequestAsync("GET", endpoint, params, entity,
new ResponseListener() {
#Override
public void onSuccess(final Response response) {
if (response != null) {
responses.add(response);
latch.countDown();
}
}
#Override
public void onFailure(final Exception exception) {
latch.countDown();
logger.error("could not get search results for....",exception);
exception.printStackTrace();
}
});
}
}
Exception here:
org.apache.http.ConnectionClosedException: Connection closed
at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.endOfInput(HttpAsyncRequestExecutor.java:341)
at org.apache.http.impl.nio.DefaultNHttpClientConnection.consumeInput(DefaultNHttpClientConnection.java:263)
at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:81)
at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:39)
at org.apache.http.impl.nio.reactor.AbstractIODispatch.inputReady(AbstractIODispatch.java:116)
at org.apache.http.impl.nio.reactor.BaseIOReactor.readable(BaseIOReactor.java:164)
at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvent(AbstractIOReactor.java:339)
at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvents(AbstractIOReactor.java:317)
at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:278)
at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:106)
at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:590)
at java.lang.Thread.run(Thread.java:745)
I don't know what is the real cause for the connection close. The exact same request works well in kopf and returns valid search results.
Plus, I don't call any restClient.close() or anything similar.
Any ideas where the problem might be?
Is the end of the input the cause to get a closed connection state (according to org.apache.http.nio.protocol.HttpAsyncRequestExecutor.endOfInput(conn))? If yes, what input is that?
Thanks
UPDATE:
I suspect the problem is related to Tomcat's HttpClient, because this code works correctly in an integration test (that is, returns results)... But it does not work (get the same ConnectionClosedException) when I make a REST request through a tomcat deployed "interface"
Any lights on this?
The problem was the port was wrong. For REST requests the port should be 9200 (and not 9300 like it was configured). More info on elasticsearch ports.
I wish elasticsearch could make a more explicit error log or a hint like "are you connecting with the right port?" for when one tries to access the 9300 port with anything other than the built-in clients.
I have an Spring + CXF application which consumes a Transmission API: Transmission RPC running in another server.
According to Transmission docs, you need to send a token which is generated on the first request. The server then responds with a 409 http code along with a header containing the token. This token should be sent on all subsequent calls:
2.3.1. CSRF Protection Most Transmission RPC servers require a X-Transmission-Session-Id header to be sent with requests, to prevent
CSRF attacks. When your request has the wrong id -- such as when you
send your first request, or when the server expires the CSRF token --
the Transmission RPC server will return an HTTP 409 error with the
right X-Transmission-Session-Id in its own headers. So, the correct
way to handle a 409 response is to update your
X-Transmission-Session-Id and to resend the previous request.
I was looking for solution either using a CXF filter or interceptor, that basically will handle the 409 response and retry the initial request adding the token header. I'm thinking that clients can persist this token and send it in future calls.
I'm not very familiar with cxf so I was wondering if this can be accomplish and how. Any hint would be helpful.
Thanks!
Here spring-retry can be utilized which is now an independent project and no longer part of spring-batch.
As explained here retry callback will help make another call updated with the token header.
Pseudo code / logic in this case would look something like below
RetryTemplate template = new RetryTemplate();
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
/*
* 1. Check if RetryContext contains the token via hasAttribute. If available set the header else proceed
* 2. Call the transmission API
* 3.a. If API responds with 409, read the token
* 3.a.1. Store the token in RetryContext via setAttribute method
* 3.a.2. Throw a custom exception so that retry kicks in
* 3.b. If API response is non 409 handle according to business logic
* 4. Return result
*/
}
});
Make sure to configure the RetryTemplate with reasonable retry & backoff policies so as to avoid any resource contention / surprises.
Let know in comments in case of any queries / roadblock.
N.B.: RetryContext's implementation RetryContextSupport has the hasAttribute & setAttribute method inherited from Spring core AttributeAccessor
Assuming you are using Apache CXF JAX RS Client it is easy to do by just creating a custom Runtime Exception and ResponseExceptionMapper for it. So the idea is to manually convert 409 outcomes to some exception and then handle them correctly (in your case retry the service call).
See following code snipped for fully working example.
#SpringBootApplication
#EnableJaxRsProxyClient
public class SpringBootClientApplication {
// This can e stored somewhere in db or elsewhere
private static String lastToken = "";
public static void main(String[] args) {
SpringApplication.run(SpringBootClientApplication.class, args);
}
#Bean
CommandLineRunner initWebClientRunner(final TransmissionService service) {
return new CommandLineRunner() {
#Override
public void run(String... runArgs) throws Exception {
try {
System.out.println(service.sayHello(1, lastToken));
// catch the TokenExpiredException get the new token and retry
} catch (TokenExpiredException ex) {
lastToken = ex.getNewToken();
System.out.println(service.sayHello(1, lastToken));
}
}
};
}
public static class TokenExpiredException extends RuntimeException {
private String newToken;
public TokenExpiredException(String token) {
newToken = token;
}
public String getNewToken() {
return newToken;
}
}
/**
* This is where the magic is done !!!!
*/
#Provider
public static class TokenExpiredExceptionMapper implements ResponseExceptionMapper<TokenExpiredException> {
#Override
public TokenExpiredException fromResponse(Response r) {
if (r.getStatus() == 409) {
return new TokenExpiredException(r.getHeaderString("X-Transmission-Session-Id"));
}
return null;
}
}
#Path("/post")
public interface TransmissionService {
#GET
#Path("/{a}")
#Produces(MediaType.APPLICATION_JSON_VALUE)
String sayHello(#PathParam("a") Integer a, #HeaderParam("X-Transmission-Session-Id") String sessionId)
throws TokenExpiredException;
}
}
My Jersey CORS request is not functioning for POST, but works for GET requests. The headers are being mapped to Jersey requests as shown in the below screenshot of a GET request to the same resource.
However, doing a POST to the below method makes me end up with XMLHttpRequest cannot load http://production.local/api/workstation. Origin http://workstation.local:81 is not allowed by Access-Control-Allow-Origin.
Here's a screenshot of network activity:
Details on failed POST request:
Here's my resource:
#Path("/workstation")
#Consumes({MediaType.APPLICATION_JSON})
#Produces({MediaType.APPLICATION_JSON})
public class WorkstationResource {
#InjectParam
WorkstationService workstationService;
#POST
public WorkstationEntity save (WorkstationEntity workstationEntity) {
workstationService.save(workstationEntity);
return workstationEntity;
}
#GET
#Path("/getAllActive")
public Collection<WorkflowEntity> getActive () {
List<WorkflowEntity> workflowEntities = new ArrayList<WorkflowEntity>();
for(Workflow workflow : Production.getWorkflowList()) {
workflowEntities.add(workflow.getEntity());
}
return workflowEntities;
}
}
My CORS filter:
public class ResponseCorsFilter implements ContainerResponseFilter {
#Override
public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
Response.ResponseBuilder responseBuilder = Response.fromResponse(response.getResponse());
responseBuilder
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, HEAD");
String reqHead = request.getHeaderValue("Access-Control-Request-Headers");
if(null != reqHead && !reqHead.equals(null)){
responseBuilder.header("Access-Control-Allow-Headers", reqHead);
}
response.setResponse(responseBuilder.build());
return response;
}
}
My Jersey configuration in my Main class:
//add jersey servlet support
ServletRegistration jerseyServletRegistration = ctx.addServlet("JerseyServlet", new SpringServlet());
jerseyServletRegistration.setInitParameter("com.sun.jersey.config.property.packages", "com.production.resource");
jerseyServletRegistration.setInitParameter("com.sun.jersey.spi.container.ContainerResponseFilters", "com.production.resource.ResponseCorsFilter");
jerseyServletRegistration.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", Boolean.TRUE.toString());
jerseyServletRegistration.setInitParameter("com.sun.jersey.config.feature.DisableWADL", Boolean.TRUE.toString());
jerseyServletRegistration.setLoadOnStartup(1);
jerseyServletRegistration.addMapping("/api/*");
While I thought this was a CORS issue, turns out it was a Jersey issue...
org.glassfish.grizzly.servlet.ServletHandler on line 256 handles an exception...
FilterChainInvoker filterChain = getFilterChain(request);
if (filterChain != null) {
filterChain.invokeFilterChain(servletRequest, servletResponse);
} else {
servletInstance.service(servletRequest, servletResponse);
}
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "service exception:", ex);
customizeErrorPage(response, "Internal Error", 500);
}
In my log, all I see is service exception: with nothing after it. When I debug this line, I end up seeing the error javax.servlet.ServletException: org.codehaus.jackson.map.JsonMappingException: Conflicting setter definitions for property "workflowProcess": com.production.model.entity.WorkstationEntity#setWorkflowProcess(1 params) vs com.production.model.entity.WorkstationEntity#setWorkflowProcess(1 params) which gives me something I can actually work with.
It's hard to tell and hard to debug since it's the browser that produces that error upon inspecting the response (header).
Even upon very close inspection your code looks fine and sane except that Access-Control-Allow-Headers is or may be set twice in filter(). While RFC 2616 (HTTP 1.1) Section 4.2 does basically permit it given certain conditions are met I wouldn't gamble here. You have no control over how browser X version N handles this.
Instead of setting the same header twice with different values rather append the 2nd set of values to the existing header.
Oh hello there, fellow SO members,
I have a web service that returns XML data using a simple get request that goes like this :
http://my-service:8082/qc/getData?paramX=0169¶mY=2
the service returns raw xml in the page according to the parameters' values.
I am trying to retrieve this data from a GET request in GWT using RequestBuilder, Request, etc.
However, the response gives me empty text, a Status code of ZERO (which doesn't mean anything and isn't supposed to happen), and so on.
Here's the simplified code that doesn't work.
public class SimpleXML implements EntryPoint {
public void onModuleLoad() {
this.doGet("http://my-service:8082/qc/getData", "0169", "2");
}
public void doGet(String serviceURL, String paramX, String paramY) {
final String getUrl = serviceURL + "?paramX=" + paramX + "&idTarification=" + paramY;
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, getUrl);
try {
Request response = builder.sendRequest(null, new RequestCallback() {
#Override
public void onResponseReceived(Request request, Response response) {
response.getStatusCode(); // Gives me 0 (zero) :(
}
#Override
public void onError(Request request, Throwable exception) {
// ... doesn't matter for this example
}
});
} catch (RequestException e) {
// ... doesn't matter for this example
}
}
}
I don't get why this wouldn't work, since this is REALLY simple, I've seen tutorials and they all show me this way of doing things..
Thanks in advance
The reason is, that browsers do not allow cross-site requests with AJAX (see Same Origin Policy).
This means, that you can only call a service on the same server, same port (using the same protocol) as your HTML page. If you want to perform cross-site requests, you can use JSONP, as explained in http://code.google.com/webtoolkit/doc/latest/tutorial/Xsite.html.