Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.atmosphere.cpr.AtmosphereRequestImpl;
import org.atmosphere.cpr.AtmosphereResponseImpl;
import org.atmosphere.cpr.BroadcasterConfig;
import org.atmosphere.cpr.HeaderConfig;
import org.atmosphere.util.VoidAnnotationProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -51,6 +52,7 @@
import com.vaadin.flow.server.VaadinServletService;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.shared.communication.PushConstants;
import com.vaadin.flow.shared.ui.Transport;

/**
* Handles requests to open a push (bidirectional) communication channel between
Expand All @@ -66,6 +68,14 @@
public class PushRequestHandler
implements RequestHandler, SessionExpiredHandler {

/**
* Response header that asks a proxy not to buffer the response. NGINX
* honours it per response, which is what the server-sent events transport
* needs: with buffering left on, an NGINX terminating TLS holds the whole
* event stream and no push message reaches the browser.
*/
private static final String ACCEL_BUFFERING_HEADER = "X-Accel-Buffering";

private AtmosphereFramework atmosphere;
private PushHandler pushHandler;

Expand Down Expand Up @@ -285,6 +295,7 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
"Atmosphere initialization failed. No push available.");
return true;
}
disableProxyBufferingForServerSentEvents(request, response);
try {
atmosphere.doCometSupport(
AtmosphereRequestImpl
Expand All @@ -303,6 +314,46 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
return true;
}

/**
* Marks a server-sent events push response as one a proxy must not buffer.
* <p>
* Must run before Atmosphere writes anything, because the header cannot be
* added once the response is committed. Other transports are left alone:
* they either complete each response, so a proxy flushes it anyway, or are
* not HTTP responses at all.
*
* @param request
* the push request
* @param response
* the response to mark
*/
static void disableProxyBufferingForServerSentEvents(VaadinRequest request,
VaadinResponse response) {
if (isServerSentEventsRequest(request)) {
response.setHeader(ACCEL_BUFFERING_HEADER, "no");
}
}

/**
* Checks whether a push request opens a server-sent events connection.
* <p>
* The client passes the transport as a query parameter, but an Atmosphere
* client may send it as a header instead, so both are read.
*
* @param request
* the push request
* @return {@code true} if the request uses the server-sent events transport
*/
private static boolean isServerSentEventsRequest(VaadinRequest request) {
String transport = request
.getParameter(HeaderConfig.X_ATMOSPHERE_TRANSPORT);
if (transport == null) {
transport = request.getHeader(HeaderConfig.X_ATMOSPHERE_TRANSPORT);
}
return Transport.SERVER_SENT_EVENTS.getIdentifier()
.equalsIgnoreCase(transport);
}

/**
* Frees any resources currently in use.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ public enum Transport {
* {@code com.vaadin.experimental.ssePushTransport} feature flag to be
* enabled. Selecting it while the feature flag is disabled throws a
* {@code DisabledFeatureException}.
* <p>
* The event stream is an ordinary HTTP response that stays open, so over
* HTTP/1.1 every open tab holds one of the connections a browser allows per
* origin, six of them in Chrome. With that many tabs of the same
* application open, the next one fails to load at all rather than merely
* losing push. Serving the application over HTTP/2, where requests share a
* single connection, lifts the limit. {@link #WEBSOCKET} and
* {@link #WEBSOCKET_XHR} are unaffected, because browsers pool WebSocket
* connections separately.
*/
SERVER_SENT_EVENTS("sse");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2000-2026 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.server.communication;

import org.atmosphere.cpr.HeaderConfig;
import org.junit.jupiter.api.Test;

import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
import com.vaadin.flow.shared.ui.Transport;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class PushRequestHandlerTest {

@Test
void serverSentEventsRequest_proxyBufferingDisabled() {
// The client puts the transport in the query string, an Atmosphere
// client may put it in a header instead.
assertBufferingDisabled(requestWithParameter(
Transport.SERVER_SENT_EVENTS.getIdentifier()));
assertBufferingDisabled(requestWithHeader(
Transport.SERVER_SENT_EVENTS.getIdentifier()));
}

@Test
void otherTransports_responseNotTouched() {
assertBufferingNotTouched(
requestWithParameter(Transport.WEBSOCKET.getIdentifier()));
assertBufferingNotTouched(
requestWithParameter(Transport.LONG_POLLING.getIdentifier()));
assertBufferingNotTouched(requestWithParameter(null));
}

private static void assertBufferingDisabled(VaadinRequest request) {
VaadinResponse response = mock(VaadinResponse.class);

PushRequestHandler.disableProxyBufferingForServerSentEvents(request,
response);

verify(response).setHeader("X-Accel-Buffering", "no");
}

private static void assertBufferingNotTouched(VaadinRequest request) {
VaadinResponse response = mock(VaadinResponse.class);

PushRequestHandler.disableProxyBufferingForServerSentEvents(request,
response);

verify(response, never()).setHeader(anyString(), any());
}

private static VaadinRequest requestWithParameter(String transport) {
VaadinRequest request = mock(VaadinRequest.class);
when(request.getParameter(HeaderConfig.X_ATMOSPHERE_TRANSPORT))
.thenReturn(transport);
return request;
}

private static VaadinRequest requestWithHeader(String transport) {
VaadinRequest request = mock(VaadinRequest.class);
when(request.getHeader(HeaderConfig.X_ATMOSPHERE_TRANSPORT))
.thenReturn(transport);
return request;
}
}
Loading