Merge remote-tracking branch 'origin/jetty-12.1.x' into experiment/jetty-12.1.x/delayedDispatch

# Conflicts:
#	jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java
#	jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java
#	jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ServletTest.java
#	jetty-ee11/jetty-ee11-servlet/src/test/java/org/eclipse/jetty/ee11/servlet/ServletTest.java
#	jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ServletTest.java
This commit is contained in:
gregw 2024-09-13 08:47:18 +10:00
commit b7c8bcb92f
862 changed files with 14356 additions and 8228 deletions

View File

@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Jetty Security Reports
url: https://eclipse.dev/jetty/security_reports.php
url: https://jetty.org/security.html
about: Please raise security issues here.

View File

@ -18,7 +18,7 @@ labels: Bug
**OS type/version**
**Description**
<!-- Do not report security issues here! See [Jetty Security Reports](https://eclipse.dev/jetty/security_reports.php) -->
<!-- Do not report security issues here! See [Jetty Security Reports](https://jetty.org/security.html) -->
**How to reproduce?**

View File

@ -56,7 +56,7 @@ This release process will produce releases:
- [ ] Merge release branches back to main branches and delete release branches.
- [ ] Verify release existence in Maven Central by triggering the Jenkins builds of CometD.
- [ ] Update Jetty versions on the website ( follow instructions in [jetty-website](https://github.com/eclipse/jetty-website/blob/master/README.md) ).
+ [ ] Update (or check) [Download](https://eclipse.dev/jetty/download.php) page is updated.
+ [ ] Update (or check) [Download](https://jetty.org/download.html) page is updated.
+ [ ] Update (or check) documentation page(s) are updated.
- [ ] Publish GitHub Releases.
- [ ] Prepare release announcement for mailing lists.

21
Jenkinsfile vendored
View File

@ -18,7 +18,7 @@ pipeline {
stage("Build / Test - JDK21") {
agent { node { label 'linux' } }
steps {
timeout( time: 180, unit: 'MINUTES' ) {
timeout( time: 210, unit: 'MINUTES' ) {
checkout scm
mavenBuild( "jdk21", "clean install -Dspotbugs.skip=true -Djacoco.skip=true", "maven3")
recordIssues id: "jdk21", name: "Static Analysis jdk21", aggregatingResults: true, enabledForFailure: true,
@ -31,7 +31,7 @@ pipeline {
stage("Build / Test - JDK22") {
agent { node { label 'linux' } }
steps {
timeout( time: 180, unit: 'MINUTES' ) {
timeout( time: 210, unit: 'MINUTES' ) {
checkout scm
mavenBuild( "jdk22", "clean install -Dspotbugs.skip=true -Djacoco.skip=true", "maven3")
recordIssues id: "jdk22", name: "Static Analysis jdk22", aggregatingResults: true, enabledForFailure: true, tools: [mavenConsole(), java(), checkStyle(), javaDoc()]
@ -39,10 +39,21 @@ pipeline {
}
}
stage("Build / Test - JDK17") {
stage("Build / Test - JDK23") {
agent { node { label 'linux' } }
steps {
timeout( time: 180, unit: 'MINUTES' ) {
checkout scm
mavenBuild( "jdk23", "clean install -Dspotbugs.skip=true -Djacoco.skip=true", "maven3")
recordIssues id: "jdk23", name: "Static Analysis jdk23", aggregatingResults: true, enabledForFailure: true, tools: [mavenConsole(), java(), checkStyle(), javaDoc()]
}
}
}
stage("Build / Test - JDK17") {
agent { node { label 'linux' } }
steps {
timeout( time: 210, unit: 'MINUTES' ) {
checkout scm
mavenBuild( "jdk17", "clean install -Perrorprone", "maven3") // javadoc:javadoc
recordIssues id: "analysis-jdk17", name: "Static Analysis jdk17", aggregatingResults: true, enabledForFailure: true,
@ -114,11 +125,11 @@ def mavenBuild(jdk, cmdline, mvnName) {
buildCache = useBuildCache()
if (buildCache) {
echo "Using build cache"
extraArgs = " -Dmaven.build.cache.restoreGeneratedSources=false -Dmaven.build.cache.remote.url=http://nginx-cache-service.jenkins.svc.cluster.local:80 -Dmaven.build.cache.remote.enabled=true -Dmaven.build.cache.remote.save.enabled=true -Dmaven.build.cache.remote.server.id=remote-build-cache-server -Daether.connector.http.supportWebDav=true "
extraArgs = " -Dmaven.build.cache.restoreGeneratedSources=false -Dmaven.build.cache.remote.url=http://nexus-service.nexus.svc.cluster.local:8081/repository/maven-build-cache -Dmaven.build.cache.remote.enabled=true -Dmaven.build.cache.remote.save.enabled=true -Dmaven.build.cache.remote.server.id=nexus-cred "
} else {
// when not using cache
echo "Not using build cache"
extraArgs = " -Dmaven.test.failure.ignore=true -Dmaven.build.cache.skipCache=true -Dmaven.build.cache.remote.url=http://nginx-cache-service.jenkins.svc.cluster.local:80 -Dmaven.build.cache.remote.enabled=true -Dmaven.build.cache.remote.save.enabled=true -Dmaven.build.cache.remote.server.id=remote-build-cache-server -Daether.connector.http.supportWebDav=true "
extraArgs = " -Dmaven.test.failure.ignore=true -Dmaven.build.cache.skipCache=true -Dmaven.build.cache.remote.url=http://nexus-service.nexus.svc.cluster.local:8081/repository/maven-build-cache -Dmaven.build.cache.remote.enabled=true -Dmaven.build.cache.remote.save.enabled=true -Dmaven.build.cache.remote.server.id=nexus-cred "
}
if (env.BRANCH_NAME ==~ /PR-\d+/) {
if (pullRequest.labels.contains("build-all-tests")) {

View File

@ -17,11 +17,9 @@ nav:
- modules/programming-guide/nav.adoc
ext:
collector:
- run:
command: mvn install -ntp -B -Dcollector -Pfast -am -pl documentation/jetty
scan:
dir: documentation/jetty/target/collector
- run: mvn install -ntp -B -Dcollector -Pfast -am -pl documentation/jetty
scan: ./target/collector
- scan:
dir: jetty-core/jetty-server/src/main/java
files: org/eclipse/jetty/server/CustomRequestLog.java
base: modules/code/partials
into: modules/code/partials

View File

@ -13,12 +13,34 @@
= Eclipse Jetty {page-version}
This section of the site contains the documentation for {page-component-title} {page-version}.
This is the main documentation page for the Eclipse Jetty Project.
Jetty provides a highly scalable and memory-efficient web server and Servlet container, supporting web protocols such as HTTP/1.1, HTTP/2, HTTP/3 and WebSocket.
Furthermore, Jetty offers integrations with many other technologies, such as OSGi, JMX, JNDI, JAAS, CDI, etc. and with the relevant Jakarta EE technologies.
Jetty is open source and are freely available for commercial use and distribution under either the link:https://www.eclipse.org/legal/epl-2.0/[Eclipse Public License v2] or the link:https://www.apache.org/licenses/LICENSE-2.0[Apache License v2].
Jetty can either be used as a standalone server to deploy web applications, or as a library that can be used in your code as a dependency.
.Jetty Versions and Compatibilities
[cols="1a,1a,1a,1a", options="header"]
|===
| Jetty Version | Required Java Version | Jakarta EE Version | Status
| Jetty 12.1.x | Java 17 | Jakarta EE11, EE10, EE9, EE8 | Development
| Jetty 12.0.x | Java 17 | Jakarta EE10, EE9, EE8 | Stable
| Jetty 11.0.x | Java 11 | Jakarta EE9 | EOL (see link:https://github.com/jetty/jetty.project/issues/10485[#10485])
| Jetty 10.0.x | Java 11 | Jakarta EE8 | EOL (see link:https://github.com/jetty/jetty.project/issues/10485[#10485])
| Jetty 9.4.x | Java 8 | Jakarta EE7 | EOL (see link:https://github.com/jetty/jetty.project/issues/7958[#7958])
|===
== xref:operations-guide:index.adoc[]
The Eclipse Jetty Operations Guide targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications.
The Operations Guide targets sysops, devops, and developers who want to install Jetty as a standalone server to deploy web applications.
== xref:programming-guide:index.adoc[]
The Eclipse Jetty Programming Guide targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications.
The Programming Guide targets developers who want to use the Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications.

View File

@ -26,6 +26,10 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-ethereum</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-infinispan-embedded-query</artifactId>
@ -54,6 +58,10 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-session</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-unixdomain-server</artifactId>

View File

@ -17,17 +17,29 @@ import java.util.concurrent.Executors;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.VirtualThreadPool;
@SuppressWarnings("unused")
public class ArchitectureDocs
{
public void configureVirtualThreads()
public void queuedVirtualThreads()
{
// tag::virtual[]
// tag::queuedVirtual[]
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setVirtualThreadsExecutor(Executors.newVirtualThreadPerTaskExecutor());
Server server = new Server(threadPool);
// end::virtual[]
// end::queuedVirtual[]
}
public void virtualVirtualThreads()
{
// tag::virtualVirtual[]
VirtualThreadPool threadPool = new VirtualThreadPool();
// Limit the max number of current virtual threads.
threadPool.setMaxThreads(200);
Server server = new Server(threadPool);
// end::virtualVirtual[]
}
}

View File

@ -329,11 +329,11 @@ public class ContentDocs
// Read a chunk.
chunk = source.read();
// No chunk, demand to be called back when there will be more chunks.
// If no chunk, schedule a demand callback when there are more chunks.
if (chunk == null)
{
source.demand(this::iterate);
return Action.IDLE;
source.demand(this::succeeded);
return Action.SCHEDULED;
}
// The read failed, re-throw the failure
@ -341,7 +341,7 @@ public class ContentDocs
if (Content.Chunk.isFailure(chunk))
throw chunk.getFailure();
// Copy the chunk.
// Copy the chunk by scheduling an asynchronous write.
sink.write(chunk.isLast(), chunk.getByteBuffer(), this);
return Action.SCHEDULED;
}
@ -349,8 +349,9 @@ public class ContentDocs
@Override
protected void onSuccess()
{
// After every successful write, release the chunk.
chunk.release();
// After every successful write, release the chunk
// and reset to the next chunk
chunk = Content.Chunk.releaseAndNext(chunk);
}
@Override
@ -360,15 +361,21 @@ public class ContentDocs
callback.succeeded();
}
@Override
protected void onFailure(Throwable cause)
{
// The copy is failed, fail the callback.
// This method is invoked before a write() has completed, so
// the chunk is not released here, but in onCompleteFailure().
callback.failed(cause);
}
@Override
protected void onCompleteFailure(Throwable failure)
{
// In case of a failure, either on the
// read or on the write, release the chunk.
chunk.release();
// The copy is failed, fail the callback.
callback.failed(failure);
// In case of a failure, this method is invoked when the write()
// is completed, and it is now possible to release the chunk.
chunk = Content.Chunk.releaseAndNext(chunk);
}
@Override

View File

@ -24,9 +24,12 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.ConnectionStatistics;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.Retainable;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.io.SelectorManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@ -225,11 +228,13 @@ public class SelectorManagerDocs
// tag::echo-correct[]
class EchoConnection extends AbstractConnection
{
private final ByteBufferPool.Sized pool;
private final IteratingCallback callback = new EchoIteratingCallback();
public EchoConnection(EndPoint endp, Executor executor)
public EchoConnection(EndPoint endp, ByteBufferPool.Sized pool, Executor executor)
{
super(endp, executor);
this.pool = pool;
}
@Override
@ -250,20 +255,20 @@ public class SelectorManagerDocs
class EchoIteratingCallback extends IteratingCallback
{
private ByteBuffer buffer;
private RetainableByteBuffer buffer;
@Override
protected Action process() throws Throwable
{
// Obtain a buffer if we don't already have one.
if (buffer == null)
buffer = BufferUtil.allocate(1024);
buffer = pool.acquire();
int filled = getEndPoint().fill(buffer);
int filled = getEndPoint().fill(buffer.getByteBuffer());
if (filled > 0)
{
// We have filled some bytes, echo them back.
getEndPoint().write(this, buffer);
getEndPoint().write(this, buffer.getByteBuffer());
// Signal that the iteration should resume
// when the write() operation is completed.
@ -273,14 +278,15 @@ public class SelectorManagerDocs
{
// We don't need the buffer anymore, so
// don't keep it around while we are idle.
buffer = null;
buffer = Retainable.release(buffer);
// No more bytes to read, declare
// again interest for fill events.
fillInterested();
fillInterested(this);
// Signal that the iteration is now IDLE.
return Action.IDLE;
// Signal that the iteration is now SCHEDULED
// for a fillable callback.
return Action.SCHEDULED;
}
else
{
@ -291,17 +297,11 @@ public class SelectorManagerDocs
}
@Override
protected void onCompleteSuccess()
protected void onCompleted(Throwable cause)
{
// The iteration completed successfully.
getEndPoint().close();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
// The iteration completed with a failure.
// The iteration completed.
getEndPoint().close(cause);
buffer = Retainable.release(buffer);
}
@Override

View File

@ -523,14 +523,16 @@ public class WebSocketDocs
@Override
public void succeed()
{
// When the send succeeds, succeed this IteratingCallback.
// Map the o.e.j.websocket.api.Callback to o.e.j.util.Callback.
// When the send() succeeds, succeed this IteratingCallback.
succeeded();
}
@Override
public void fail(Throwable x)
{
// When the send fails, fail this IteratingCallback.
// Map the o.e.j.websocket.api.Callback to o.e.j.util.Callback.
// When the send() fails, fail this IteratingCallback.
failed(x);
}

View File

@ -17,6 +17,7 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.Path;
@ -34,6 +35,7 @@ import org.eclipse.jetty.client.BasicAuthentication;
import org.eclipse.jetty.client.BufferingResponseListener;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.CompletableResponseListener;
import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.ConnectionPool;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.Destination;
@ -72,6 +74,7 @@ import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.Transport;
import org.eclipse.jetty.io.ssl.SslHandshakeListener;
import org.eclipse.jetty.quic.client.ClientQuicConfiguration;
@ -1049,6 +1052,31 @@ public class HTTPClientDocs
// end::setConnectionPool[]
}
public void preCreateConnections() throws Exception
{
// tag::preCreateConnections[]
HttpClient httpClient = new HttpClient();
httpClient.start();
// For HTTP/1.1, you need to explicitly configure to initialize connections.
if (httpClient.getTransport() instanceof HttpClientTransportOverHTTP http1)
http1.setInitializeConnections(true);
// Create a dummy request to the server you want to pre-create connections to.
Request request = httpClient.newRequest("https://host/");
// Resolve the destination for that request.
Destination destination = httpClient.resolveDestination(request);
// Pre-create, for example, half of the connections.
int preCreate = httpClient.getMaxConnectionsPerDestination() / 2;
CompletableFuture<Void> completable = destination.getConnectionPool().preCreateConnections(preCreate);
// Wait for the connections to be created.
completable.get(5, TimeUnit.SECONDS);
// end::preCreateConnections[]
}
public void unixDomain() throws Exception
{
// tag::unixDomain[]
@ -1155,4 +1183,29 @@ public class HTTPClientDocs
.send();
// end::mixedTransports[]
}
public void connectionInformation() throws Exception
{
// tag::connectionInformation[]
HttpClient httpClient = new HttpClient();
httpClient.start();
ContentResponse response = httpClient.newRequest("http://domain.com/path")
// The connection information is only available starting from the request begin event.
.onRequestBegin(request ->
{
Connection connection = request.getConnection();
// Obtain the address of the server.
SocketAddress remoteAddress = connection.getRemoteSocketAddress();
System.getLogger("connection").log(INFO, "Server address: %s", remoteAddress);
// Obtain the SslSessionData.
EndPoint.SslSessionData sslSessionData = connection.getSslSessionData();
if (sslSessionData != null)
System.getLogger("connection").log(INFO, "SslSessionData: %s", sslSessionData);
})
.send();
// end::connectionInformation[]
}
}

View File

@ -0,0 +1,43 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.docs.programming.security.siwe;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.siwe.EthereumAuthenticator;
import org.eclipse.jetty.server.Handler;
public class SignInWithEthereum
{
public static SecurityHandler createSecurityHandler(Handler handler)
{
// tag::configureSecurityHandler[]
// This uses jetty-core, but you can configure a ConstraintSecurityHandler for use with EE10.
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
securityHandler.setHandler(handler);
securityHandler.put("/*", Constraint.ANY_USER);
// Add the EthereumAuthenticator to the securityHandler.
EthereumAuthenticator authenticator = new EthereumAuthenticator();
securityHandler.setAuthenticator(authenticator);
// In embedded you can configure via EthereumAuthenticator APIs.
authenticator.setLoginPath("/login.html");
// Or you can configure with parameters on the SecurityHandler.
securityHandler.setParameter(EthereumAuthenticator.LOGIN_PATH_PARAM, "/login.html");
// end::configureSecurityHandler[]
return securityHandler;
}
}

View File

@ -0,0 +1,117 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.docs.programming.security.siwe;
import java.io.PrintWriter;
import java.nio.file.Paths;
import java.util.Objects;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.siwe.EthereumAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.session.SessionHandler;
import org.eclipse.jetty.util.Callback;
public class SignInWithEthereumEmbeddedExample
{
public static void main(String[] args) throws Exception
{
Server server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(8080);
server.addConnector(connector);
String resourcePath = Paths.get(Objects.requireNonNull(SignInWithEthereumEmbeddedExample.class.getClassLoader().getResource("")).toURI())
.resolve("../../src/main/resources/")
.normalize().toString();
System.err.println(resourcePath);
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setDirAllowed(false);
resourceHandler.setBaseResourceAsString(resourcePath);
Handler.Abstract handler = new Handler.Wrapper(resourceHandler)
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
String pathInContext = Request.getPathInContext(request);
if ("/login.html".equals(pathInContext))
{
return super.handle(request, response, callback);
}
else if ("/logout".equals(pathInContext))
{
AuthenticationState.logout(request, response);
Response.sendRedirect(request, response, callback, "/");
callback.succeeded();
return true;
}
AuthenticationState authState = Objects.requireNonNull(AuthenticationState.getAuthenticationState(request));
response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html");
try (PrintWriter writer = new PrintWriter(Content.Sink.asOutputStream(response)))
{
writer.write("UserPrincipal: " + authState.getUserPrincipal());
writer.write("<br><a href=\"/logout\">Logout</a>");
}
callback.succeeded();
return true;
}
};
SecurityHandler securityHandler = createSecurityHandler(handler);
SessionHandler sessionHandler = new SessionHandler();
sessionHandler.setHandler(securityHandler);
ContextHandler contextHandler = new ContextHandler();
contextHandler.setContextPath("/");
contextHandler.setHandler(sessionHandler);
server.setHandler(contextHandler);
server.start();
server.join();
}
public static SecurityHandler createSecurityHandler(Handler handler)
{
// tag::configureSecurityHandler[]
// This uses jetty-core, but you can configure a ConstraintSecurityHandler for use with EE10.
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
securityHandler.setHandler(handler);
securityHandler.put("/*", Constraint.ANY_USER);
// Add the EthereumAuthenticator to the securityHandler.
EthereumAuthenticator authenticator = new EthereumAuthenticator();
securityHandler.setAuthenticator(authenticator);
// In embedded you can configure via EthereumAuthenticator APIs.
authenticator.setLoginPath("/login.html");
// Or you can configure with parameters on the SecurityHandler.
securityHandler.setParameter(EthereumAuthenticator.LOGIN_PATH_PARAM, "/login.html");
// end::configureSecurityHandler[]
return securityHandler;
}
}

View File

@ -176,6 +176,7 @@ public class ServerDocs
@Override
public void onFillable()
{
// Called from fillInterested() in onOpen() to start iteration.
callback.iterate();
}
@ -206,11 +207,8 @@ public class ServerDocs
// the application completed the request processing.
return Action.SCHEDULED;
}
else
{
// Did not receive enough JSON bytes,
// loop around to try to read more.
}
// Did not receive enough JSON bytes to complete the
// JSON parsing, loop around to try to read more bytes.
}
else if (filled == 0)
{
@ -218,12 +216,11 @@ public class ServerDocs
// don't keep it around while we are idle.
buffer = null;
// No more bytes to read, declare
// again interest for fill events.
fillInterested();
// No more bytes to read, declare again interest for fill events.
fillInterested(this);
// Signal that the iteration is now IDLE.
return Action.IDLE;
// Signal that the iteration is now SCHEDULED for fill interest callback.
return Action.SCHEDULED;
}
else
{

View File

@ -95,7 +95,7 @@ public class SessionDocs
org.eclipse.jetty.session.SessionHandler sessionHandler = new org.eclipse.jetty.session.SessionHandler();
sessionHandler.setSessionCookie("SIMPLE");
sessionHandler.setUsingCookies(true);
sessionHandler.setUsingURLs(false);
sessionHandler.setUsingUriParameters(false);
sessionHandler.setSessionPath("/");
server.setHandler(sessionHandler);
sessionHandler.setHandler(new Handler.Abstract()

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign-In with Ethereum</title>
<script src="https://cdn.jsdelivr.net/npm/web3@1.6.1/dist/web3.min.js"></script>
</head>
<body>
<h4>Sign-In with Ethereum</h4>
<button id="siwe">Sign-In with Ethereum</button>
<form id="loginForm" action="/auth/login" method="POST" style="display: none;">
<input type="hidden" id="signatureField" name="signature">
<input type="hidden" id="messageField" name="message">
</form>
<p class="alert" style="display: none;">Result: <span id="siweResult"></span></p>
<script>
let provider = window.ethereum;
let accounts;
if (!provider) {
document.getElementById('siweResult').innerText = 'MetaMask is not installed. Please install MetaMask to use this feature.';
} else {
document.getElementById('siwe').addEventListener('click', async () => {
try {
// Request account access if needed
accounts = await provider.request({ method: 'eth_requestAccounts' });
const domain = window.location.host;
const from = accounts[0];
// Fetch nonce from the server
const nonceResponse = await fetch('/auth/nonce');
const nonceData = await nonceResponse.json();
const nonce = nonceData.nonce;
const siweMessage = `${domain} wants you to sign in with your Ethereum account:\n${from}\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://${domain}\nVersion: 1\nChain ID: 1\nNonce: ${nonce}\nIssued At: ${new Date().toISOString()}`;
const signature = await provider.request({
method: 'personal_sign',
params: [siweMessage, from]
});
console.log("signature: " + signature)
console.log("nonce: " + nonce)
console.log("length: " + length)
document.getElementById('signatureField').value = signature;
document.getElementById('messageField').value = siweMessage;
document.getElementById('loginForm').submit();
} catch (error) {
console.error('Error during login:', error);
document.getElementById('siweResult').innerText = `Error: ${error.message}`;
document.getElementById('siweResult').parentElement.style.display = 'block';
}
});
}
</script>
</body>
</html>

View File

@ -708,6 +708,34 @@ If you want to use virtual threads, introduced as a preview feature in Java 19 a
See also the xref:server/index.adoc#threadpool[section about configuring the thread pool].
[[threadpool-all-virtual]]
== Module `threadpool-all-virtual`
The `threadpool-all-virtual` module allows you to configure the server-wide thread pool, similarly to what you can do with the <<threadpool,`threadpool`>> Jetty module, so that all threads are virtual threads, introduced as an official feature since Java 21.
CAUTION: Only use this module if you are using Java 21 or later.
If you are using Java 19 or Java 20, use the <<threadpool-virtual-preview,`threadpool-virtual-preview`>> Jetty module instead.
The module properties to configure the thread pool are:
----
include::{jetty-home}/modules/threadpool-all-virtual.mod[tags=documentation]
----
The property `jetty.threadpool.maxThreads` limits, using a `Semaphore`, the number of current virtual threads in use.
Limiting the number of current virtual threads helps to limit resource usage in applications, especially in case of load spikes.
When an unlimited number of virtual threads is allowed, the server might be brought down due to resource (typically memory) exhaustion.
[CAUTION]
====
Even when using virtual threads, Jetty uses non-blocking I/O, and dedicates a thread to each `java.nio.channels.Selector` to perform the `Selector.select()` operation.
Currently (up to Java 22), calling `Selector.select()` from a virtual thread pins the carrier thread.
When using the `threadpool-all-virtual` Jetty module, if you have `N` selectors, then `N` carrier threads will be pinned by the virtual threads calling `Selector.select()`, possibly making your system less efficient, and at worst locking up the entire system if there are no carrier threads available to run virtual threads.
====
[[threadpool-virtual]]
== Module `threadpool-virtual`

View File

@ -328,32 +328,30 @@ Virtual threads have been introduced as a preview feature in Java 19 and Java 20
The xref:modules/standard.adoc#threadpool-virtual-preview[`threadpool-virtual-preview`] Jetty module provides support for virtual threads in Java 19 and Java 20, and it is mutually exclusive with the `threadpool` Jetty module.
The xref:modules/standard.adoc#threadpool-virtual[`threadpool-virtual`] Jetty module provides support for virtual threads in Java 21 or later, and it is mutually exclusive with the `threadpool` Jetty module.
When using Java 21, there are two Jetty modules available:
* xref:modules/standard.adoc#threadpool-virtual[`threadpool-virtual`]
* xref:modules/standard.adoc#threadpool-all-virtual[`threadpool-all-virtual`]
Both are mutually exclusive with the `threadpool` Jetty module.
If you have already enabled the `threadpool` Jetty module, it is sufficient to remove it by removing the `$JETTY_BASE/start.d/threadpool.ini` file.
When using Java 21 or later, you can enable the xref:modules/standard.adoc#threadpool-virtual[`threadpool-virtual`] module:
The xref:modules/standard.adoc#threadpool-virtual[`threadpool-virtual`] Jetty module provides a mixed thread mode, where platform threads are used to run internal Jetty tasks, but application code is invoked using virtual threads.
The xref:modules/standard.adoc#threadpool-all-virtual[`threadpool-all-virtual`] Jetty module provides a thread mode where all threads are virtual threads, including those used internally by Jetty.
You can enable either module using:
----
$ java -jar $JETTY_HOME/start.jar --add-modules=threadpool-virtual,http
----
After the command above, the `$JETTY_BASE` directory looks like this:
or
[source]
----
$JETTY_BASE
├── resources
│ └── jetty-logging.properties
└── start.d
├── http.ini
└── threadpool-virtual.ini
$ java -jar $JETTY_HOME/start.jar --add-modules=threadpool-all-virtual,http
----
Now you can customize the `threadpool-virtual.ini` file to explicitly configure the thread pool and the virtual threads and then start Jetty:
[jetty%nowrap]
....
[jetty]
setupArgs=--add-modules=threadpool-virtual,http
....
After the command above, the `$JETTY_BASE/start.d/` directory will contain the corresponding `threadpool-virtual.ini` or `threadpool-all-virtual.ini` file.
You can now explicitly configure the thread pool module properties inside the `+*.ini+` file and then start Jetty.

View File

@ -43,6 +43,9 @@
** xref:troubleshooting/state-tracking.adoc[]
** xref:troubleshooting/component-dump.adoc[]
** xref:troubleshooting/debugging.adoc[]
* Jetty Security
** xref:security/siwe-support.adoc[]
* Migration Guides
** xref:migration/94-to-10.adoc[]
** xref:migration/11-to-12.adoc[]
** xref:migration/12.0-to-12.1.adoc[]

View File

@ -187,7 +187,7 @@ In turn, this calls `IteratingCallback.process()`, an abstract method that must
Method `process()` must return:
* `Action.SCHEDULED`, to indicate whether the loop has performed a non-blocking, possibly asynchronous, operation
* `Action.IDLE`, to indicate that the loop should temporarily be suspended to be resumed later
* `Action.IDLE`, to indicate that the loop should temporarily be suspended to be resumed later with another call to iterate
* `Action.SUCCEEDED` to indicate that the loop exited successfully
Any exception thrown within `process()` exits the loops with a failure.
@ -209,13 +209,18 @@ If this was the only active network connection, the system would now be idle, wi
Eventually, the Jetty I/O system will notify that the `write()` completed; this notifies the `IteratingCallback` that can now resume the loop and call `process()` again.
When `process()` is called, it is possible that zero bytes are read from the network; in this case, you want to deallocate the buffer since the other peer may never send more bytes for the `Connection` to read, or it may send them after a long pause -- in both cases we do not want to retain the memory allocated by the buffer; next, you want to call `fillInterested()` to declare again interest for read events, and return `Action.IDLE` since there is nothing to write back and therefore the loop may be suspended.
When more bytes are again available to be read from the network, `onFillable()` will be called again and that will start the iteration again.
When `process()` is called, it is possible that zero bytes are read from the network; in this case, you want to deallocate the buffer since the other peer may never send more bytes for the `Connection` to read, or it may send them after a long pause -- in both cases we do not want to retain the memory allocated by the buffer; next, you want to call `fillInterested(this)` to declare again interest for read events, and return `Action.SCHEDULED` since a callback is scheduled to occur once filling is possible.
Another possibility is that during `process()` the read returns `-1` indicating that the other peer has closed the connection; this means that there will not be more bytes to read and the loop can be exited, so you return `Action.SUCCEEDED`; `IteratingCallback` will then call `onCompleteSuccess()` where you can close the `EndPoint`.
The last case is that during `process()` an exception is thrown, for example by `EndPoint.fill(ByteBuffer)` or, in more advanced implementations, by code that parses the bytes that have been read and finds them unacceptable; any exception thrown within `process()` will be caught by `IteratingCallback` that will exit the loop with a failure and call `onCompleteFailure(Throwable)` with the exception that has been thrown, where you can close the `EndPoint`, passing the exception that is the reason for closing prematurely the `EndPoint`.
Note that some failures may occur whilst a scheduled operation is in progress.
Such failures are notified immediately via the `onFailure(Throwable)` method, but care must be taken to not release any resources that may still be in use by the scheduled operation.
The `onCompleteFailure(Throwable)` method is called when both a failure has occurred and any scheduled operation has completed.
An example of this issue is that a buffer used for a write operation cannot be returned to a pool in `onFailure(Throwable)` as the write may still be progressing.
Either the buffer must be removed from the pool in `onFailure(Throwable)` or the release of the buffer deferred until `onCompleteFailure(Throwable)` is called.
[IMPORTANT]
====
Asynchronous programming is hard.
@ -356,9 +361,9 @@ You must initiate a second write only when the first is finished, for example:
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=sinkMany]
----
When you need to perform an unknown number of writes, you must use an `IteratingCallback`, explained in <<echo,this section>>, to avoid ``StackOverFlowError``s.
When you need to perform an unknown number of writes, you may use an `IteratingCallback`, explained in <<echo,this section>>, to avoid ``StackOverFlowError``s.
For example, to copy from a `Content.Source` to a `Content.Sink` you should use the convenience method `Content.copy(Content.Source, Content.Sink, Callback)`.
For example, to copy from a `Content.Source` to a `Content.Sink` you could use the convenience method `Content.copy(Content.Source, Content.Sink, Callback)`.
For illustrative purposes, below you can find the implementation of `copy(Content.Source, Content.Sink, Callback)` that uses an `IteratingCallback`:
[,java,indent=0]

View File

@ -235,11 +235,14 @@ Virtual threads have been introduced in Java 19 and Java 20 as a preview feature
NOTE: In Java versions where virtual threads are a preview feature, remember to add `+--enable-preview+` to the JVM command line options to use virtual threads.
[[thread-pool-virtual-threads-queued]]
==== Virtual Threads Support with `QueuedThreadPool`
`QueuedThreadPool` can be configured to use virtual threads by specifying the virtual threads `Executor`:
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java[tags=virtual]
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java[tags=queuedVirtual]
----
[CAUTION]
@ -255,3 +258,17 @@ Enabling virtual threads in `QueuedThreadPool` will default the number of reserv
Defaulting the number of reserved threads to zero ensures that the <<execution-strategy-pec,Produce-Execute-Consume mode>> is always used, which means that virtual threads will always be used for blocking tasks.
====
[[thread-pool-virtual-threads-virtual]]
==== Virtual Threads Support with `VirtualThreadPool`
`VirtualThreadPool` is an alternative to `QueuedThreadPool` that creates only virtual threads (no platform threads).
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java[tags=virtualVirtual]
----
Despite the name, `VirtualThreadPool` does not pool virtual threads, but allows you to impose a limit on the maximum number of current virtual threads, in order to limit resource consumption.
Furthermore, you can configure it to track virtual threads so that a xref:troubleshooting/component-dump.adoc[Jetty component dump] will show all virtual threads, including those that are unmounted.

View File

@ -158,7 +158,7 @@ Jetty's client library provides the following `ConnectionPool` implementations:
* `DuplexConnectionPool`, historically the first implementation, only used by the HTTP/1.1 transport.
* `MultiplexConnectionPool`, the generic implementation valid for any transport where connections are reused with a most recently used algorithm (that is, the connections most recently returned to the connection pool are the more likely to be used again).
* `RoundRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with a round-robin algorithm.
* `RandomRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly.
* `RandomConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly.
The `ConnectionPool` implementation can be customized for each destination in by setting a `ConnectionPool.Factory` on the `HttpClientTransport`:
@ -167,6 +167,34 @@ The `ConnectionPool` implementation can be customized for each destination in by
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=setConnectionPool]
----
[[connection-pool-precreate-connections]]
=== Pre-Creating Connections
`ConnectionPool` offers the ability to pre-create connections by calling `ConnectionPool.preCreateConnections(int)`.
Pre-creating the connections saves the time and processing spent to establish the TCP connection, performing the TLS handshake (if necessary) and, for HTTP/2 and HTTP/3, perform the initial protocol setup.
This is particularly important for HTTP/2 because in the initial protocol setup the server informs the client of the maximum number of concurrent requests per connection (otherwise assumed to be just `1` by the client).
The scenarios where pre-creating connections is useful are, for example:
* Load testing, where you want to prepare the system with connections already created to avoid paying of cost of connection setup.
* Proxying scenarios, often in conjunction with the use of `RoundRobinConnectionPool` or `RandomConnectionPool`, where the proxy creates early the connections to the backend servers.
This is an example of how to pre-create connections:
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=preCreateConnections]
----
[NOTE]
====
Pre-creating connections for secure HTTP/1.1 requires you to call `HttpClientTransportOverHTTP.setInitializeConnections(true)`, otherwise only the TCP connection is established, but the TLS handshake is not initiated.
To initialize connections for secure HTTP/1.1, the client sends an initial `OPTIONS * HTTP/1.1` request to the server.
The server must be able to handle this request without closing the connection (in particular it must not add the `Connection: close` header in the response).
====
[[request-processing]]
== HttpClient Request Processing
@ -207,7 +235,12 @@ A second request with the same origin sent _after_ the first request/response cy
A second request with the same origin sent _concurrently_ with the first request will likely cause the opening of a second connection, depending on the connection pool implementation.
The configuration parameter `HttpClient.maxConnectionsPerDestination` (see also the <<configuration,configuration section>>) controls the max number of connections that can be opened for a destination.
NOTE: If opening connections to a given origin takes a long time, then requests for that origin will queue up in the corresponding destination until the connections are established.
[NOTE]
====
If opening connections to a given origin takes a long time, then requests for that origin will queue up in the corresponding destination until the connections are established.
To save the time spent opening connections, you can xref:connection-pool-precreate-connections[pre-create connections].
====
Each connection can handle a limited number of concurrent requests.
For HTTP/1.1, this number is always `1`: there can only be one outstanding request for each connection.
@ -500,6 +533,28 @@ This is a fancy example of how to mix HTTP versions and low-level transports:
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=mixedTransports]
----
[[connection-information]]
=== Request Connection Information
In order to send a request, it is necessary to obtain a connection, as explained in the xref:request-processing[request processing section].
The HTTP/1.1 protocol may send only one request at a time on a single connection, while multiplexed protocols such as HTTP/2 may send many requests at a time on a single connection.
You can access the connection information, for example the local and remote `SocketAddress`, or the `SslSessionData` if the connection is secured, in the following way:
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=connectionInformation]
----
[NOTE]
====
The connection information is only available when the request is associated with a connection.
This means that the connection is not available in the _request queued_ event, but only starting from the _request begin_ event.
For more information about request events, see xref:non-blocking[this section].
====
[[configuration]]
== HttpClient Configuration

View File

@ -23,3 +23,24 @@ You may use the xref:client/index.adoc[Jetty client-side library] in your applic
Likewise, you may use the xref:server/index.adoc[Jetty server-side library] to quickly create an HTTP or REST service without having to create a web application archive file (a `+*.war+` file) and without having to deploy it to a Jetty standalone server that you would have to download and install.
This guide will walk you through the design of the Jetty libraries and how to use its classes to write your applications.
== Code Deprecation Policy
As the Jetty code evolves, classes and/or methods are deprecated using the `@Deprecated` annotation and will be removed in a future Jetty release.
The Jetty release numbering follows this scheme: `<major>.<minor>.<micro>`. For example, 12.0.5 has `major=12`, `minor=0` and `micro=5`.
As much as possible, deprecated code is not removed in micro releases.
Deprecated code may be removed in major releases.
Deprecated code may be removed in minor releases, but only if it has been deprecated for at least 6 micro releases.
For example, let's assume that Jetty 12.1.0 (a new minor release) is released after the release of Jetty 12.0.11.
Then, code that was deprecated in Jetty 12.0.5 or earlier may be removed from Jetty 12.1.0 (because it has been deprecated for more than 6 micro releases).
On the other hand, code that was deprecated in Jetty 12.0.8 may be removed in Jetty 12.1.3 (because it has been deprecated for 3 micro releases in Jetty 12.0.x, and for 3 micro releases in Jetty 12.1.x -- 12.1.0, 12.1.1 and 12.1.2).
[NOTE]
====
There could be rare cases where code (possibly not even deprecated) must be removed earlier than specified above to address security vulnerabilities.
====

View File

@ -99,6 +99,15 @@
| `org.eclipse.jetty.websocket.api.**WebSocketPolicy**` | `org.eclipse.jetty.websocket.api.**Configurable**`
|===
== Server-Side Web Application APIs Changes
Jetty 12 introduced redesigned server-side APIs for web applications.
In Jetty 11, these APIs were based on a mix of Jakarta Servlet APIs and Jetty Handler APIs, while in Jetty 12 they are solely based on Jetty Handler APIs.
In Jetty 12 you can now write web applications independently of the Servlet APIs, so you can migrate Jakarta Servlets to Jetty Handlers as explained in xref:servlet-to-handler[this section].
If you were already using the Jetty 11 Handler APIs, you can migrate them to the Jetty 12 Handler APIs as explained in xref:api-changes-handler[this section].
[[servlet-to-handler]]
== Migrate Servlets to Jetty Handlers
@ -126,6 +135,8 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-source]
----
Refer also to the `Content.Source` APIs detailed in xref:arch/io.adoc#content-source[this section].
=== Handler Response APIs
[,java,indent=0]
----
@ -150,9 +161,48 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-trailers]
----
Refer also to the `Content.Sink` APIs detailed in xref:arch/io.adoc#content-sink[this section].
[[api-changes]]
== APIs Changes
[[api-changes-handler]]
=== `Handler`
The server-side `Handler` class, and the APIs to use for request/response processing, have been redesigned in Jetty 12.
The Jetty 11 `Handler` method:
`Handler.handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)`
has been changed in Jetty 12 to:
`Handler.handle(Request request, Response response, Callback callback)`
The Jetty 11 `target` parameter has been removed, and in Jetty 12 it has been replaced by the information present in `Request.getHttpURI()`.
In Jetty 11, ``Handler``s would mark the fact that they handled the request, and therefore are producing a response, by calling `Request.setHandled(true)`.
In Jetty 12, this is performed by returning `true` from the `Handler.handle(\...)` method, which also requires that the `Callback` parameter must be completed, either by succeeding it or failing it.
In Jetty 11, the `Handler.handle(\...)` method has a blocking semantic, while in Jetty 12 the `Handler.handle(\...)` method has an asynchronous semantic, thanks to the `Callback` parameter.
This means that you can return from the `Handler.handle(\...)` method _before_ the response has been sent, similarly to what you can do with the Servlet APIs when you call `HttpServletRequest.startAsync()`.
Similarly, in Jetty 11 after a call to `startAsync()` you must call `AsyncContext.complete()`, while in Jetty 12 you must complete the `Callback` parameter, either by succeeding it or failing it.
In Jetty 11, `AbstractHandler` provides a utility class to implement `Handler`.
In Jetty 12, use `Handler.Abstract`.
In Jetty 11, the APIs to deal with request or response HTTP headers are based on either Jetty's `HttpFields`, or the Servlet APIs.
In Jetty 12, the HTTP headers API are only based on `HttpFields`.
Please refer to the `HttpFields` link:{javadoc-url}/org/eclipse/jetty/http/HttpFields.html[javadocs] for details.
In Jetty 11, the request content is accessed via `Request.getInputStream()` or `HttpServletRequest.getInputStream()`.
In Jetty 12, the `Request` object itself _is-a_ `Content.Source` that can be read as explained in xref:arch/io.adoc#content-source[this section].
In Jetty 12, you can use `Content.Source.asInputStream(request)` to obtain an `InputStream` and minimize the changes to your code, but remember that `InputStream` only provides blocking APIs, while `Content.Source` provides non-blocking APIs.
In Jetty 11, the response content is accessed via `Response.getOutputStream()` or `HttpServletResponse.getOutputStream()`.
In Jetty 12, the `Response` object itself _is-a_ `Content.Sink` that can be written as explained in xref:arch/io.adoc#content-sink[this section].
In Jetty 12, you can use `Content.Sink.asOutputStream(response)` to obtain an `OutputStream` and minimize the changes to your code, but remember that `OutputStream` only provides blocking APIs, while `Content.Sink` provides non-blocking APIs.
=== `HttpClient`
The Jetty 11 `Request.onResponseContentDemanded(Response.DemandedContentListener)` API has been replaced by `Request.onResponseContentSource(Response.ContentSourceListener)` in Jetty 12.
@ -165,6 +215,8 @@ The Jetty 12 model is a "demand+pull" model: when the content is available, the
For more information about the new model, see xref:arch/io.adoc#content-source[this section].
Jetty 12 introduced the concept of low-level transport for high-level protocols, described in xref:client/io-arch.adoc#transport[this section].
=== WebSocket
The Jetty WebSocket APIs have been vastly simplified, and brought in line with the style of other APIs.

View File

@ -0,0 +1,27 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
= Migrating from Jetty 12.0.x to Jetty 12.1.x
[[api-changes]]
== APIs Changes
=== `IteratingCallback`
Class `IteratingCallback` underwent refinements that changed the behavior of the `onCompleteFailure(Throwable)` method.
In Jetty 12.0.x, `IteratingCallback.onCompleteFailure(Throwable)` was called as soon as a failure was reported, without waiting the completion of the asynchronous operation (despite its name containing the word "complete").
For example, if a write operation performed with `IteratingCallback` was pending due to TCP congestion, and a timeout happened, `onCompleteFailure(Throwable)` was called as soon as the timeout happened, without waiting for the TCP congestion to resolve.
In Jetty 12.1.x, the same behavior is achieved by `IteratingCallback.onFailure(Throwable)`, so applications should review their usage of `IteratingCallback` and change the overrides of `onCompleteFailure(Throwable)` to override `onFailure(Throwable)` instead.

View File

@ -0,0 +1,180 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
[[siwe-support]]
= SIWE Support
== Introduction
Sign-In with Ethereum (SIWE) is a decentralized authentication protocol that allows users to authenticate using their Ethereum account.
This enables users to retain more control over their identity and provides an alternative to protocols such as OpenID Connect, which rely on a centralized identity provider.
Sign-In with Ethereum works by using off-chain services to sign a standard message format defined by EIP-4361 (https://eips.ethereum.org/EIPS/eip-4361). The user signs the SIWE message to prove ownership of the Ethereum address. This is verified by the server by extracting the Ethereum address from the signature and comparing it to the address supplied in the SIWE message.
Typically, you would rely on a browser extension such as MetaMask to provide a user-friendly way for users to sign the message with their Ethereum account.
=== Support
Currently Jetty only provides support SIWE in Jetty 12.1+ and only for `jetty-core`, and `ee10`+ environments. It is enabled by adding the `EtheremAuthenticator` to the `SecurityHandler` of your web application.
== Usage
=== Enabling SIWE
The Sign-In with Ethereum module can be enabled when using Standalone Jetty with.
[source,subs=attributes+]
----
$ java -jar $JETTY_HOME/start.jar --add-modules=siwe
----
If using embedded Jetty you must add the `EthereumAuthenticator` to your `SecurityHandler`.
=== Configuration
Configuration of the `EthereumAuthenticator` is done through init params on the `ServletContext` or `SecurityHandler`. The `loginPath` is the only mandatory configuration and the others have defaults that you may wish to configure.
Login Path::
* Init param: `org.eclipse.jetty.security.siwe.login_path`
* Description: Unauthenticated requests are redirected to a login page where they must sign a SIWE message and send it to the server. This path represents a page in the application that contains the SIWE login page.
Nonce Path::
* Init param: `org.eclipse.jetty.security.siwe.nonce_path`
* Description: Requests to this path will generate a random nonce string which is associated with the session. The nonce is used in the SIWE Message to avoid replay attacks. The path at which this nonce is served can be configured through the init parameter. The application does not need to implement their own nonce endpoint, they just configure this path and the Authenticator handles it. The default value for this is `/auth/nonce` if left un-configured.
Authentication Path::
* Init param: `org.eclipse.jetty.security.siwe.authentication_path`
* Description: The authentication path is where requests containing a signed SIWE message are sent in order to authenticate the user. The default value for this is `/auth/login`.
Max Message Size::
* Init Param: `org.eclipse.jetty.security.siwe.max_message_size`
* Description: This is the max size of the authentication message which can be read by the implementation. This limit defaults to `4 * 1024`. This is necessary because the complete request content is read into a string and then parsed.
Logout Redirect Path::
* Init Param: `org.eclipse.jetty.security.siwe.logout_redirect_path`
* Description: Where the request is redirected to after logout. If left un-configured no redirect will be done upon logout.
Error Path::
* Init Param: `org.eclipse.jetty.security.siwe.error_path`
* Description: Path where Authentication errors are sent, this may contain an optional query string. An error description is available on the error page through the request parameter `error_description_jetty`. If this configuration is not set Jetty will send a 403 Forbidden response upon authentication errors.
Dispatch::
* Init Param: `org.eclipse.jetty.security.siwe.dispatch`
* Description: If set to true a dispatch will be done instead of a redirect to the login page in the case of an unauthenticated request. This defaults to false.
Authenticate New Users::
* Init Param: `org.eclipse.jetty.security.siwe.authenticate_new_users`
* Description: This can be set to false if you have a nested `LoginService` and only want to authenticate users known by the `LoginService`. This defaults to `true` meaning that any user will be authenticated regardless if they are known by the nested `LoginService`.
Domains::
* Init Param: org.eclipse.jetty.security.siwe.domains
* Description: This list of allowed domains to be declared in the `domain` field of the SIWE Message. If left blank this will allow all domains.
Chain IDs::
* Init Param: org.eclipse.jetty.security.siwe.chainIds
* Description: This list of allowed Chain IDs to be declared in the `chain-id` field of the SIWE Message. If left blank this will allow all Chain IDs.
=== Nested LoginService
A nested `LoginService` may be used to assign roles to users of a known Ethereum Address. Or the nested `LoginService` may be combined with the setting `authenticateNewUsers == false` to only allow authentication of known users.
For example a `HashLoginService` may be configured through the `jetty-ee10-web.xml` file:
[, xml, indent=0]
----
<Configure id="wac" class="org.eclipse.jetty.ee10.webapp.WebAppContext">
<Call id="ResourceFactory" class="org.eclipse.jetty.util.resource.ResourceFactory" name="of">
<Arg><Ref refid="Server"/></Arg>
<Call id="realmResource" name="newResource">
<Arg><SystemProperty name="jetty.base" default="."/>/etc/realm.properties</Arg>
</Call>
</Call>
<Call name="getSecurityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.HashLoginService">
<Set name="name">myRealm</Set>
<Set name="config"><Ref refid="realmResource"/></Set>
</New>
</Set>
</Call>
</Configure>
----
=== Application Implementation
EIP-4361 specifies the format of a SIWE Message, the overview of the Sign-In with Ethereum process, and message validation. However, it does not specify certain things like how the SIWE Message and signature are sent to the server for validation, and it does not specify the process the client acquires the nonce from the server. For this reason the `EthereumAuthenticator` has been made extensible to allow different implementations.
Currently Jetty supports authentication requests of type `application/x-www-form-urlencoded` or `multipart/form-data`, which contains the fields `message` and `signature`. Where `message` contains the full SIWE message, and `signature` is the ERC-1271 signature of the SIWE message.
The nonce endpoint provided by the `EthereumAuthenticator` returns a response with `application/json` format, with a single key of `nonce`.
=== Configuring Security Handler
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/security/siwe/SignInWithEthereumEmbeddedExample.java[tags=configureSecurityHandler]
----
=== Login Page Example
Include the `Web3.js` library to interact with the users Ethereum wallet.
[,html,indent=0]
----
<script src="https://cdn.jsdelivr.net/npm/web3@1.6.1/dist/web3.min.js"></script>
----
HTML form to submit the sign in request.
[,html,indent=0]
----
<button id="siwe">Sign-In with Ethereum</button>
<form id="loginForm" action="/auth/login" method="POST" style="display: none;">
<input type="hidden" id="signatureField" name="signature">
<input type="hidden" id="messageField" name="message">
</form>
<p class="alert" style="display: none;">Result: <span id="siweResult"></span></p>
----
Add script to generate and sign the SIWE message when the sign-in button is pressed.
[,html,indent=0]
----
<script>
let provider = window.ethereum;
let accounts;
if (!provider) {
document.getElementById('siweResult').innerText = 'MetaMask is not installed. Please install MetaMask to use this feature.';
} else {
document.getElementById('siwe').addEventListener('click', async () => {
try {
accounts = await provider.request({ method: 'eth_requestAccounts' });
const domain = window.location.host;
const from = accounts[0];
// Fetch nonce from the server.
const nonceResponse = await fetch('/auth/nonce');
const nonceData = await nonceResponse.json();
const nonce = nonceData.nonce;
const siweMessage = `${domain} wants you to sign in with your Ethereum account:\n${from}\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://${domain}\nVersion: 1\nChain ID: 1\nNonce: ${nonce}\nIssued At: ${new Date().toISOString()}`;
document.getElementById('signatureField').value = await provider.request({
method: 'personal_sign',
params: [siweMessage, from]
});
document.getElementById('messageField').value = siweMessage;
document.getElementById('loginForm').submit();
} catch (error) {
console.error('Error during login:', error);
document.getElementById('siweResult').innerText = `Error: ${error.message}`;
document.getElementById('siweResult').parentElement.style.display = 'block';
}
});
}
</script>
----

View File

@ -57,8 +57,7 @@ public class ConscryptHTTP2ClientTest
sslContextFactory.setProvider("Conscrypt");
Conscrypt.setDefaultHostnameVerifier((certs, hostname, session) -> true);
HTTP2Client client = new HTTP2Client();
try
try (HTTP2Client client = new HTTP2Client())
{
client.addBean(sslContextFactory);
client.start();
@ -97,10 +96,6 @@ public class ConscryptHTTP2ClientTest
assertTrue(latch.await(15, TimeUnit.SECONDS));
}
finally
{
client.stop();
}
}
private boolean canConnectTo(String host, int port)

View File

@ -132,17 +132,12 @@ public class ConscryptHTTP2ServerTest
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSslContextFactory(newClientSslContextFactory());
HTTP2Client h2Client = new HTTP2Client(clientConnector);
HttpClient client = new HttpClient(new HttpClientTransportOverHTTP2(h2Client));
client.start();
try
try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP2(h2Client)))
{
client.start();
int port = ((ServerConnector)server.getConnectors()[0]).getLocalPort();
ContentResponse contentResponse = client.GET("https://localhost:" + port);
assertEquals(200, contentResponse.getStatus());
}
finally
{
client.stop();
}
}
}

View File

@ -45,8 +45,7 @@ public class JDK9HTTP2ClientTest
Assumptions.assumeTrue(canConnectTo(host, port));
HTTP2Client client = new HTTP2Client();
try
try (HTTP2Client client = new HTTP2Client())
{
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
client.addBean(sslContextFactory);
@ -87,10 +86,6 @@ public class JDK9HTTP2ClientTest
latch.await(15, TimeUnit.SECONDS);
}
finally
{
client.stop();
}
}
private boolean canConnectTo(String host, int port)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">

View File

@ -55,6 +55,11 @@
<artifactId>jetty-deploy</artifactId>
<version>12.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-ethereum</artifactId>
<version>12.0.13-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>

View File

@ -1,8 +1,12 @@
# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
[description]
Adds the Jetty HTTP client to the server classpath.
Adds the Jetty HTTP client dependencies to the server classpath.
[tags]
client
[lib]
lib/jetty-client-${jetty.version}.jar
lib/jetty-alpn-client-${jetty.version}.jar
lib/jetty-alpn-java-client-${jetty.version}.jar
lib/jetty-client-${jetty.version}.jar

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.client;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import java.util.Map;
@ -73,10 +72,4 @@ public abstract class AbstractConnectorHttpClientTransport extends AbstractHttpC
context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, connector);
destination.getOrigin().getTransport().connect(address, context);
}
@Override
public void connect(InetSocketAddress address, Map<String, Object> context)
{
connect((SocketAddress)address, context);
}
}

View File

@ -127,7 +127,8 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
{
// The request may still be sending content, stop it.
Request request = response.getRequest();
request.abort(new HttpRequestException("Aborting request after receiving a %d response".formatted(response.getStatus()), request));
if (request.getBody() != null)
request.abort(new HttpRequestException("Aborting request after receiving a %d response".formatted(response.getStatus()), request));
}
@Override

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.client;
import java.io.Closeable;
import java.net.SocketAddress;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Promise;
/**
@ -63,4 +64,13 @@ public interface Connection extends Closeable
{
return null;
}
/**
* @return the {@link EndPoint.SslSessionData} associated with
* the connection, or {@code null} if the connection is not secure.
*/
default EndPoint.SslSessionData getSslSessionData()
{
return null;
}
}

View File

@ -39,10 +39,14 @@ public class ContinueProtocolHandler implements ProtocolHandler
@Override
public boolean accept(Request request, Response response)
{
boolean is100 = response.getStatus() == HttpStatus.CONTINUE_100;
boolean expect100 = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
boolean handled100 = request.getAttributes().containsKey(ATTRIBUTE);
return (is100 || expect100) && !handled100;
if (handled100)
return false;
boolean is100 = response.getStatus() == HttpStatus.CONTINUE_100;
if (is100)
return true;
// Also handle non-100 responses, because we need to complete the request to complete the whole exchange.
return request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
}
@Override

View File

@ -1,121 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.client;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.client.internal.HttpContentResponse;
/**
* A {@link BufferingResponseListener} that is also a {@link Future}, to allow applications
* to block (indefinitely or for a timeout) until {@link #onComplete(Result)} is called,
* or to {@link #cancel(boolean) abort} the request/response conversation.
* <p>
* Typical usage is:
* <pre>
* Request request = httpClient.newRequest(...)...;
* FutureResponseListener listener = new FutureResponseListener(request);
* request.send(listener); // Asynchronous send
* ContentResponse response = listener.get(5, TimeUnit.SECONDS); // Timed block
* </pre>
*
* @deprecated Use {@link CompletableResponseListener} instead
*/
@Deprecated
public class FutureResponseListener extends BufferingResponseListener implements Future<ContentResponse>
{
private final AtomicBoolean cancelled = new AtomicBoolean();
private final CountDownLatch latch = new CountDownLatch(1);
private final Request request;
private ContentResponse response;
private Throwable failure;
public FutureResponseListener(Request request)
{
this(request, 2 * 1024 * 1024);
}
public FutureResponseListener(Request request, int maxLength)
{
super(maxLength);
this.request = request;
}
public Request getRequest()
{
return request;
}
@Override
public void onComplete(Result result)
{
response = new HttpContentResponse(result.getResponse(), getContent(), getMediaType(), getEncoding());
failure = result.getFailure();
latch.countDown();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
if (cancelled.compareAndSet(false, true))
{
request.abort(new CancellationException());
return true;
}
return false;
}
@Override
public boolean isCancelled()
{
return cancelled.get();
}
@Override
public boolean isDone()
{
return latch.getCount() == 0 || isCancelled();
}
@Override
public ContentResponse get() throws InterruptedException, ExecutionException
{
latch.await();
return getResult();
}
@Override
public ContentResponse get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
boolean expired = !latch.await(timeout, unit);
if (expired)
throw new TimeoutException();
return getResult();
}
private ContentResponse getResult() throws ExecutionException
{
if (isCancelled())
throw (CancellationException)new CancellationException().initCause(failure);
if (failure != null)
throw new ExecutionException(failure);
return response;
}
}

View File

@ -20,13 +20,14 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.IO;
/**
* {@link ContentDecoder} for the "gzip" encoding.
*/
public class GZIPContentDecoder extends org.eclipse.jetty.http.GZIPContentDecoder implements ContentDecoder
{
public static final int DEFAULT_BUFFER_SIZE = 8192;
public static final int DEFAULT_BUFFER_SIZE = IO.DEFAULT_BUFFER_SIZE;
private long decodedLength;

View File

@ -104,7 +104,7 @@ import org.slf4j.LoggerFactory;
* }</pre>
*/
@ManagedObject("The HTTP client")
public class HttpClient extends ContainerLifeCycle
public class HttpClient extends ContainerLifeCycle implements AutoCloseable
{
public static final String USER_AGENT = "Jetty/" + Jetty.VERSION;
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -471,13 +471,7 @@ public class HttpClient extends ContainerLifeCycle
port = normalizePort(scheme, port);
Transport transport = request.getTransport();
if (transport == null)
{
// Ask the ClientConnector for backwards compatibility
// until ClientConnector.Configurator is removed.
transport = connector.newTransport();
if (transport == null)
transport = Transport.TCP_IP;
}
transport = Transport.TCP_IP;
return new Origin(scheme, new Origin.Address(host, port), request.getTag(), protocol, transport);
}
@ -1141,4 +1135,10 @@ public class HttpClient extends ContainerLifeCycle
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory);
}
@Override
public void close() throws Exception
{
stop();
}
}

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.client;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Map;
@ -71,24 +70,8 @@ public interface HttpClientTransport extends ClientConnectionFactory
*
* @param address the address to connect to
* @param context the context information to establish the connection
* @deprecated use {@link #connect(SocketAddress, Map)} instead.
*/
@Deprecated
public void connect(InetSocketAddress address, Map<String, Object> context);
/**
* Establishes a physical connection to the given {@code address}.
*
* @param address the address to connect to
* @param context the context information to establish the connection
*/
public default void connect(SocketAddress address, Map<String, Object> context)
{
if (address instanceof InetSocketAddress)
connect((InetSocketAddress)address, context);
else
throw new UnsupportedOperationException("Unsupported SocketAddress " + address);
}
public void connect(SocketAddress address, Map<String, Object> context);
/**
* @return the factory for ConnectionPool instances

View File

@ -346,6 +346,12 @@ public class HttpProxy extends ProxyConfiguration.Proxy
return connection.getRemoteSocketAddress();
}
@Override
public EndPoint.SslSessionData getSslSessionData()
{
return connection.getSslSessionData();
}
@Override
public void send(Request request, Response.CompleteListener listener)
{

View File

@ -61,7 +61,8 @@ public class RedirectProtocolHandler implements ProtocolHandler, Response.Listen
{
// The request may still be sending content, stop it.
Request request = response.getRequest();
request.abort(new HttpRequestException("Aborting request after receiving a %d response".formatted(response.getStatus()), request));
if (request.getBody() != null)
request.abort(new HttpRequestException("Aborting request after receiving a %d response".formatted(response.getStatus()), request));
}
@Override

View File

@ -26,20 +26,49 @@ public class HttpClientConnectionFactory implements ClientConnectionFactory
/**
* <p>Representation of the {@code HTTP/1.1} application protocol used by {@link HttpClientTransportDynamic}.</p>
*/
public static final Info HTTP11 = new HTTP11(new HttpClientConnectionFactory());
public static final Info HTTP11 = new HTTP11();
private boolean initializeConnections;
/**
* @return whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
public boolean isInitializeConnections()
{
return initializeConnections;
}
/**
* @param initialize whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
public void setInitializeConnections(boolean initialize)
{
this.initializeConnections = initialize;
}
@Override
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context)
{
HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, context);
connection.setInitialize(isInitializeConnections());
return customize(connection, context);
}
private static class HTTP11 extends Info
/**
* <p>Representation of the {@code HTTP/1.1} application protocol used by {@link HttpClientTransportDynamic}.</p>
* <p>Applications should prefer using the constant {@link HttpClientConnectionFactory#HTTP11}, unless they
* need to customize the associated {@link HttpClientConnectionFactory}.</p>
*/
public static class HTTP11 extends Info
{
private static final List<String> protocols = List.of("http/1.1");
private HTTP11(ClientConnectionFactory factory)
public HTTP11()
{
this(new HttpClientConnectionFactory());
}
public HTTP11(ClientConnectionFactory factory)
{
super(factory);
}

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.client.transport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -91,19 +90,6 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
this(new ClientConnector(), HttpClientConnectionFactory.HTTP11);
}
/**
* <p>Creates a dynamic transport that speaks the given protocols, in order of preference
* (first the most preferred).</p>
*
* @param infos the protocols this dynamic transport speaks
* @deprecated use {@link #HttpClientTransportDynamic(ClientConnector, ClientConnectionFactory.Info...)}
*/
@Deprecated(since = "12.0.7", forRemoval = true)
public HttpClientTransportDynamic(ClientConnectionFactory.Info... infos)
{
this(findClientConnector(infos), infos);
}
/**
* <p>Creates a dynamic transport with the given {@link ClientConnector} and the given protocols,
* in order of preference (first the most preferred).</p>
@ -121,14 +107,6 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
);
}
private static ClientConnector findClientConnector(ClientConnectionFactory.Info[] infos)
{
return Arrays.stream(infos)
.flatMap(info -> info.getContainedBeans(ClientConnector.class).stream())
.findFirst()
.orElseGet(ClientConnector::new);
}
@Override
public Origin newOrigin(Request request)
{
@ -216,11 +194,7 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
Transport transport = request.getTransport();
if (transport == null)
{
// Ask the ClientConnector for backwards compatibility
// until ClientConnector.Configurator is removed.
transport = getClientConnector().newTransport();
if (transport == null)
transport = matchingInfos.get(0).newTransport();
transport = matchingInfos.get(0).newTransport();
request.transport(transport);
}

View File

@ -22,7 +22,6 @@ import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.DuplexConnectionPool;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ProcessorUtils;
@ -37,7 +36,7 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
public static final Origin.Protocol HTTP11 = new Origin.Protocol(List.of("http/1.1"), false);
private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportOverHTTP.class);
private final ClientConnectionFactory factory = new HttpClientConnectionFactory();
private final HttpClientConnectionFactory factory = new HttpClientConnectionFactory();
private int headerCacheSize = 1024;
private boolean headerCacheCaseSensitive;
@ -79,25 +78,54 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
return connection;
}
@ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache")
/**
* @return the max size in bytes for the HTTP header field cache
*/
@ManagedAttribute("The maximum allowed size in bytes for the HTTP header field cache")
public int getHeaderCacheSize()
{
return headerCacheSize;
}
/**
* @param headerCacheSize the max size in bytes for the HTTP header field cache
*/
public void setHeaderCacheSize(int headerCacheSize)
{
this.headerCacheSize = headerCacheSize;
}
@ManagedAttribute("Whether the header field cache is case sensitive")
/**
* @return whether the HTTP header field cache is case-sensitive
*/
@ManagedAttribute("Whether the HTTP header field cache is case-sensitive")
public boolean isHeaderCacheCaseSensitive()
{
return headerCacheCaseSensitive;
}
/**
* @param headerCacheCaseSensitive whether the HTTP header field cache is case-sensitive
*/
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
this.headerCacheCaseSensitive = headerCacheCaseSensitive;
}
/**
* @return whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
@ManagedAttribute("Whether newly created connections should be initialized with an OPTIONS * HTTP/1.1 request")
public boolean isInitializeConnections()
{
return factory.isInitializeConnections();
}
/**
* @param initialize whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
public void setInitializeConnections(boolean initialize)
{
factory.setInitializeConnections(initialize);
}
}

View File

@ -20,7 +20,6 @@ import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.Authentication;
import org.eclipse.jetty.client.AuthenticationStore;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpProxy;
import org.eclipse.jetty.client.HttpRequestException;
@ -35,6 +34,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.CyclicTimeouts;
import org.eclipse.jetty.util.Attachable;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
@ -146,7 +146,7 @@ public abstract class HttpConnection implements IConnection, Attachable
// Make sure the path is there
String path = request.getPath();
if (path.trim().length() == 0)
if (StringUtil.isBlank(path))
{
path = "/";
request.path(path);
@ -191,11 +191,7 @@ public abstract class HttpConnection implements IConnection, Attachable
// Add content headers.
Request.Content content = request.getBody();
if (content == null)
{
request.body(new BytesRequestContent());
}
else
if (content != null)
{
if (!headers.contains(HttpHeader.CONTENT_TYPE))
{
@ -203,10 +199,7 @@ public abstract class HttpConnection implements IConnection, Attachable
if (contentType == null)
contentType = getHttpClient().getDefaultRequestContentType();
if (contentType != null)
{
HttpField field = new HttpField(HttpHeader.CONTENT_TYPE, contentType);
request.addHeader(field);
}
request.addHeader(new HttpField(HttpHeader.CONTENT_TYPE, contentType));
}
long contentLength = content.getLength();
if (contentLength >= 0)
@ -215,6 +208,9 @@ public abstract class HttpConnection implements IConnection, Attachable
request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength));
}
}
// RFC 9110, section 10.1.1.
if (content == null || content.getLength() == 0)
request.headers(h -> h.remove(HttpHeader.EXPECT));
// Cookies.
StringBuilder cookies = convertCookies(request.getCookies(), null);
@ -243,7 +239,7 @@ public abstract class HttpConnection implements IConnection, Attachable
{
if (builder == null)
builder = new StringBuilder();
if (builder.length() > 0)
if (!builder.isEmpty())
builder.append("; ");
builder.append(cookie.getName()).append("=").append(cookie.getValue());
}

View File

@ -70,18 +70,6 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
private boolean stale;
private long activeNanoTime;
/**
* @param client the {@link HttpClient}
* @param origin the {@link Origin}
* @param intrinsicallySecure whether the destination is intrinsically secure
* @deprecated use {@link #HttpDestination(HttpClient, Origin)} instead
*/
@Deprecated(since = "12.0.7", forRemoval = true)
public HttpDestination(HttpClient client, Origin origin, boolean intrinsicallySecure)
{
this(client, origin);
}
/**
* <p>Creates a new HTTP destination.</p>
*

View File

@ -67,7 +67,7 @@ public abstract class HttpReceiver
{
private static final Logger LOG = LoggerFactory.getLogger(HttpReceiver.class);
private final SerializedInvoker invoker = new SerializedInvoker();
private final SerializedInvoker invoker = new SerializedInvoker(HttpReceiver.class);
private final HttpChannel channel;
private ResponseState responseState = ResponseState.IDLE;
private NotifiableContentSource contentSource;
@ -315,14 +315,25 @@ public abstract class HttpReceiver
* Method to be invoked when response content is available to be read.
* <p>
* This method takes care of ensuring the {@link Content.Source} passed to
* {@link Response.ContentSourceListener#onContentSource(Response, Content.Source)} calls the
* demand callback.
* {@link Response.ContentSourceListener#onContentSource(Response, Content.Source)}
* calls the demand callback.
* The call to the demand callback is serialized with other events.
*/
protected void responseContentAvailable()
protected void responseContentAvailable(HttpExchange exchange)
{
if (LOG.isDebugEnabled())
LOG.debug("Response content available on {}", this);
contentSource.onDataAvailable();
LOG.debug("Invoking responseContentAvailable on {}", this);
invoker.run(() ->
{
if (LOG.isDebugEnabled())
LOG.debug("Executing responseContentAvailable on {}", this);
if (exchange.isResponseCompleteOrTerminated())
return;
contentSource.onDataAvailable();
});
}
/**
@ -344,7 +355,7 @@ public abstract class HttpReceiver
if (!exchange.responseComplete(null))
return;
invoker.run(() ->
Runnable successTask = () ->
{
if (LOG.isDebugEnabled())
LOG.debug("Executing responseSuccess on {}", this);
@ -365,7 +376,12 @@ public abstract class HttpReceiver
// Mark atomically the response as terminated, with
// respect to concurrency between request and response.
terminateResponse(exchange);
}, afterSuccessTask);
};
if (afterSuccessTask == null)
invoker.run(successTask);
else
invoker.run(successTask, afterSuccessTask);
}
/**
@ -693,6 +709,9 @@ public abstract class HttpReceiver
current = HttpReceiver.this.read(false);
if (LOG.isDebugEnabled())
LOG.debug("Read {} from {}", current, this);
try (AutoLock ignored = lock.lock())
{
if (currentChunk != null)
@ -712,9 +731,10 @@ public abstract class HttpReceiver
{
if (LOG.isDebugEnabled())
LOG.debug("onDataAvailable on {}", this);
// The demandCallback will call read() that will itself call
// HttpReceiver.read(boolean) so it must be called by the invoker.
invokeDemandCallback(true);
invoker.assertCurrentThreadInvoking();
// The onDataAvailable() method is only ever called
// by the invoker so avoid using the invoker again.
invokeDemandCallback(false);
}
@Override
@ -736,6 +756,8 @@ public abstract class HttpReceiver
if (LOG.isDebugEnabled())
LOG.debug("Processing demand on {}", this);
invoker.assertCurrentThreadInvoking();
Content.Chunk current;
try (AutoLock ignored = lock.lock())
{
@ -760,8 +782,8 @@ public abstract class HttpReceiver
}
}
// The processDemand method is only ever called by the
// invoker so there is no need to use the latter here.
// The processDemand() method is only ever called
// by the invoker so avoid using the invoker again.
invokeDemandCallback(false);
}
@ -769,21 +791,25 @@ public abstract class HttpReceiver
{
Runnable demandCallback = demandCallbackRef.getAndSet(null);
if (LOG.isDebugEnabled())
LOG.debug("Invoking demand callback on {}", this);
if (demandCallback != null)
LOG.debug("Invoking demand callback {} on {}", demandCallback, this);
if (demandCallback == null)
return;
try
{
try
if (invoke)
{
if (invoke)
invoker.run(demandCallback);
else
demandCallback.run();
invoker.run(demandCallback);
}
catch (Throwable x)
else
{
fail(x);
invoker.assertCurrentThreadInvoking();
demandCallback.run();
}
}
catch (Throwable x)
{
fail(x);
}
}
@Override

View File

@ -386,8 +386,11 @@ public abstract class HttpSender
}
}
private void internalAbort(HttpExchange exchange, Throwable failure)
private void internalAbort(Throwable failure)
{
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return;
anyToFailure(failure);
abortRequest(exchange);
}
@ -528,7 +531,7 @@ public abstract class HttpSender
action.run();
// Read the request content.
chunk = content.read();
chunk = content != null ? content.read() : Content.Chunk.EOF;
}
if (LOG.isDebugEnabled())
LOG.debug("Content {} for {}", chunk, request);
@ -539,6 +542,7 @@ public abstract class HttpSender
{
// No content after the headers, demand.
demanded = true;
assert content != null;
content.demand(this::succeeded);
return Action.SCHEDULED;
}
@ -616,22 +620,24 @@ public abstract class HttpSender
}
@Override
protected void onCompleteFailure(Throwable x)
protected void onFailure(Throwable x)
{
if (chunk != null)
{
chunk.release();
chunk = Content.Chunk.next(chunk);
}
failRequest(x);
internalAbort(exchange, x);
internalAbort(x);
Promise<Boolean> promise = abort;
if (promise != null)
promise.succeeded(true);
}
@Override
protected void onCompleteFailure(Throwable x)
{
if (chunk != null)
chunk.release();
chunk = Content.Chunk.next(chunk);
}
@Override
public InvocationType getInvocationType()
{

View File

@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.Request;
@ -40,6 +41,7 @@ import org.eclipse.jetty.client.transport.HttpRequest;
import org.eclipse.jetty.client.transport.IConnection;
import org.eclipse.jetty.client.transport.SendFailure;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint;
@ -61,6 +63,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
private final LongAdder bytesIn = new LongAdder();
private final LongAdder bytesOut = new LongAdder();
private long idleTimeout;
private boolean initialize;
public HttpConnectionOverHTTP(EndPoint endPoint, Map<String, Object> context)
{
@ -113,6 +116,12 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
return delegate.getRemoteSocketAddress();
}
@Override
public EndPoint.SslSessionData getSslSessionData()
{
return delegate.getSslSessionData();
}
@Override
public long getBytesIn()
{
@ -159,12 +168,46 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
return delegate.send(exchange);
}
/**
* @return whether to initialize the connection with an {@code OPTIONS * HTTP/1.1} request.
*/
public boolean isInitialize()
{
return initialize;
}
/**
* @param initialize whether to initialize the connection with an {@code OPTIONS * HTTP/1.1} request.
*/
public void setInitialize(boolean initialize)
{
this.initialize = initialize;
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
promise.succeeded(this);
boolean initialize = isInitialize();
if (initialize)
{
Destination destination = getHttpDestination();
Request request = destination.getHttpClient().newRequest(destination.getOrigin().asString())
.method(HttpMethod.OPTIONS)
.path("*");
send(request, result ->
{
if (result.isSucceeded())
promise.succeeded(this);
else
promise.failed(result.getFailure());
});
}
else
{
promise.succeeded(this);
}
}
@Override
@ -313,6 +356,12 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
return getEndPoint().getRemoteSocketAddress();
}
@Override
public EndPoint.SslSessionData getSslSessionData()
{
return getEndPoint().getSslSessionData();
}
@Override
public SendFailure send(HttpExchange exchange)
{

View File

@ -43,17 +43,18 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
{
private static final Logger LOG = LoggerFactory.getLogger(HttpReceiverOverHTTP.class);
private final Runnable receiveNext = this::receiveNext;
private final LongAdder inMessages = new LongAdder();
private final HttpParser parser;
private final ByteBufferPool byteBufferPool;
private RetainableByteBuffer networkBuffer;
private boolean shutdown;
private boolean complete;
private State state = State.STATUS;
private boolean unsolicited;
private String method;
private int status;
private String method;
private Content.Chunk chunk;
private Runnable action;
private boolean shutdown;
private boolean disposed;
public HttpReceiverOverHTTP(HttpChannelOverHTTP channel)
{
@ -73,13 +74,15 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
{
if (!hasContent())
{
boolean setFillInterest = parseAndFill();
boolean setFillInterest = parseAndFill(true);
if (!hasContent() && setFillInterest)
fillInterested();
}
else
{
responseContentAvailable();
HttpExchange exchange = getHttpExchange();
if (exchange != null)
responseContentAvailable(exchange);
}
}
@ -95,10 +98,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
super.reset();
parser.reset();
if (chunk != null)
{
chunk.release();
chunk = null;
}
chunk = null;
}
@Override
@ -107,10 +108,9 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
super.dispose();
parser.close();
if (chunk != null)
{
chunk.release();
chunk = null;
}
chunk = null;
disposed = true;
}
@Override
@ -122,7 +122,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
Content.Chunk chunk = consumeChunk();
if (chunk != null)
return chunk;
boolean needFillInterest = parseAndFill();
boolean needFillInterest = parseAndFill(false);
if (LOG.isDebugEnabled())
LOG.debug("ParseAndFill needFillInterest {} in {}", needFillInterest, this);
chunk = consumeChunk();
@ -234,7 +234,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
* If this method depletes the buffer, it will always try to re-fill until fill generates 0 byte.
* @return true if no bytes were filled.
*/
private boolean parseAndFill()
private boolean parseAndFill(boolean notifyContentAvailable)
{
HttpConnectionOverHTTP connection = getHttpConnection();
EndPoint endPoint = connection.getEndPoint();
@ -244,23 +244,22 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
acquireNetworkBuffer();
while (true)
{
if (LOG.isDebugEnabled())
LOG.debug("Parsing {} in {}", networkBuffer, this);
// Always parse even empty buffers to advance the parser.
if (parse())
boolean stopParsing = parse(notifyContentAvailable);
if (LOG.isDebugEnabled())
LOG.debug("Parsed stop={} in {}", stopParsing, this);
if (stopParsing)
{
// Return immediately, as this thread may be in a race
// with e.g. another thread demanding more content.
return false;
}
if (LOG.isDebugEnabled())
LOG.debug("Parser willing to advance in {}", this);
// Connection may be closed in a parser callback.
if (connection.isClosed())
if (connection.isClosed() || isShutdown())
{
if (LOG.isDebugEnabled())
LOG.debug("Closed {} in {}", connection, this);
LOG.debug("Closed/Shutdown {} in {}", connection, this);
releaseNetworkBuffer();
return false;
}
@ -269,6 +268,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
reacquireNetworkBuffer();
// The networkBuffer may have been reacquired.
assert !networkBuffer.hasRemaining();
int read = endPoint.fill(networkBuffer.getByteBuffer());
if (LOG.isDebugEnabled())
LOG.debug("Read {} bytes in {} from {} in {}", read, networkBuffer, endPoint, this);
@ -284,9 +284,9 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
}
else
{
releaseNetworkBuffer();
shutdown();
return false;
// Loop around to parse again to advance the parser,
// for example for HTTP/1.0 connection-delimited content.
}
}
}
@ -305,62 +305,80 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
*
* @return true to indicate that parsing should be interrupted (and will be resumed by another thread).
*/
private boolean parse()
private boolean parse(boolean notifyContentAvailable)
{
// HttpParser is not reentrant, so we cannot invoke the
// application from the parser event callbacks.
// However, the mechanism in general (and this method)
// is reentrant: it notifies the application which may
// read response content, which reenters here.
ByteBuffer byteBuffer = networkBuffer.getByteBuffer();
while (true)
{
boolean handle = parser.parseNext(networkBuffer.getByteBuffer());
boolean handle = parser.parseNext(byteBuffer);
if (LOG.isDebugEnabled())
LOG.debug("Parse result={} on {}", handle, this);
Runnable action = getAndSetAction(null);
if (action != null)
LOG.debug("Parse state={} result={} {} {} on {}", state, handle, BufferUtil.toDetailString(byteBuffer), parser, this);
if (!handle)
return false;
HttpExchange exchange = getHttpExchange();
if (exchange == null)
throw new IllegalStateException("No exchange");
switch (state)
{
if (LOG.isDebugEnabled())
LOG.debug("Executing action after parser returned: {} on {}", action, this);
action.run();
if (LOG.isDebugEnabled())
LOG.debug("Action executed after Parse result={} on {}", handle, this);
}
if (handle)
{
// When the receiver is aborted, the parser is closed in dispose() which changes
// its state to State.CLOSE; so checking parser.isClose() is just a way to check
// if the receiver was aborted or not.
return !parser.isClose();
}
boolean complete = this.complete;
this.complete = false;
if (LOG.isDebugEnabled())
LOG.debug("Parse complete={}, {} {} in {}", complete, networkBuffer, parser, this);
if (complete)
{
int status = this.status;
this.status = 0;
// Connection upgrade due to 101, bail out.
if (status == HttpStatus.SWITCHING_PROTOCOLS_101)
return true;
// Connection upgrade due to CONNECT + 200, bail out.
String method = this.method;
this.method = null;
if (getHttpChannel().isTunnel(method, status))
return true;
if (networkBuffer.isEmpty())
return false;
if (!HttpStatus.isInformational(status))
case HEADERS -> responseHeaders(exchange);
case CONTENT ->
{
if (LOG.isDebugEnabled())
LOG.debug("Discarding unexpected content after response {}: {} in {}", status, networkBuffer, this);
networkBuffer.clear();
if (notifyContentAvailable)
responseContentAvailable(exchange);
}
case COMPLETE ->
{
boolean isUpgrade = status == HttpStatus.SWITCHING_PROTOCOLS_101;
boolean isTunnel = getHttpChannel().isTunnel(method, status);
Runnable task = isUpgrade || isTunnel ? null : this.receiveNext;
responseSuccess(exchange, task);
// Connection upgrade, bail out.
if (isUpgrade || isTunnel)
return true;
if (byteBuffer.hasRemaining())
{
if (HttpStatus.isInterim(status))
{
// There may be multiple interim responses in
// the same network buffer, continue parsing.
continue;
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Discarding unexpected content after response {}: {} in {}", status, BufferUtil.toDetailString(byteBuffer), this);
BufferUtil.clear(byteBuffer);
return false;
}
}
// Continue to read from the network.
return false;
}
default -> throw new IllegalStateException("Invalid state " + state);
}
// The application may have aborted the request.
if (disposed)
{
BufferUtil.clear(byteBuffer);
return false;
}
if (networkBuffer.isEmpty())
return false;
// The application has been invoked,
// and it is now driving the parsing.
return true;
}
}
@ -384,7 +402,6 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
// header, the connection will be closed at exchange termination
// thanks to the flag we have set above.
parser.atEOF();
parser.parseNext(BufferUtil.EMPTY_BUFFER);
}
protected boolean isShutdown()
@ -404,6 +421,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
this.status = status;
parser.setHeadResponse(HttpMethod.HEAD.is(method) || getHttpChannel().isTunnel(method, status));
exchange.getResponse().version(version).status(status).reason(reason);
state = State.STATUS;
responseBegin(exchange);
}
@ -430,10 +448,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
// Store the EndPoint is case of upgrades, tunnels, etc.
exchange.getRequest().getConversation().setAttribute(EndPoint.class.getName(), getHttpConnection().getEndPoint());
getHttpConnection().onResponseHeaders(exchange);
if (LOG.isDebugEnabled())
LOG.debug("Setting action to responseHeaders(exchange, boolean) on {}", this);
if (getAndSetAction(() -> responseHeaders(exchange)) != null)
throw new IllegalStateException();
state = State.HEADERS;
return true;
}
@ -449,17 +464,13 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
if (chunk != null)
throw new IllegalStateException("Content generated with unconsumed content left");
if (getHttpConnection().isFillInterested())
throw new IllegalStateException("Fill interested while parsing for content");
// Retain the chunk because it is stored for later use.
networkBuffer.retain();
chunk = Content.Chunk.asChunk(buffer, false, networkBuffer);
if (LOG.isDebugEnabled())
LOG.debug("Setting action to responseContentAvailable on {}", this);
if (getAndSetAction(this::responseContentAvailable) != null)
throw new IllegalStateException();
if (getHttpConnection().isFillInterested())
throw new IllegalStateException();
state = State.CONTENT;
return true;
}
@ -489,28 +500,20 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
if (exchange == null || unsolicited)
{
// We received an unsolicited response from the server.
networkBuffer.clear();
getHttpConnection().close();
return false;
}
int status = exchange.getResponse().getStatus();
if (!HttpStatus.isInterim(status))
{
inMessages.increment();
complete = true;
}
if (chunk != null)
throw new IllegalStateException();
chunk = Content.Chunk.EOF;
boolean isUpgrade = status == HttpStatus.SWITCHING_PROTOCOLS_101;
boolean isTunnel = getHttpChannel().isTunnel(method, status);
Runnable task = isUpgrade || isTunnel ? null : this::receiveNext;
if (LOG.isDebugEnabled())
LOG.debug("Message complete, calling response success with task {} in {}", task, this);
responseSuccess(exchange, task);
return false;
state = State.COMPLETE;
return true;
}
private void receiveNext()
@ -522,7 +525,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
if (LOG.isDebugEnabled())
LOG.debug("Receiving next request in {}", this);
boolean setFillInterest = parseAndFill();
boolean setFillInterest = parseAndFill(true);
if (!hasContent() && setFillInterest)
fillInterested();
}
@ -554,13 +557,6 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
}
}
private Runnable getAndSetAction(Runnable action)
{
Runnable r = this.action;
this.action = action;
return r;
}
long getMessagesIn()
{
return inMessages.longValue();
@ -571,4 +567,9 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
{
return String.format("%s[%s]", super.toString(), parser);
}
private enum State
{
STATUS, HEADERS, CONTENT, COMPLETE
}
}

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.Retainable;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
@ -237,7 +238,9 @@ public class HttpSenderOverHTTP extends HttpSender
@Override
protected void onSuccess()
{
release();
headerBuffer = Retainable.release(headerBuffer);
chunkBuffer = Retainable.release(chunkBuffer);
contentByteBuffer = null;
}
@Override
@ -248,21 +251,16 @@ public class HttpSenderOverHTTP extends HttpSender
}
@Override
protected void onCompleteFailure(Throwable cause)
protected void onFailure(Throwable cause)
{
super.onCompleteFailure(cause);
release();
callback.failed(cause);
}
private void release()
@Override
protected void onCompleteFailure(Throwable cause)
{
if (headerBuffer != null)
headerBuffer.release();
headerBuffer = null;
if (chunkBuffer != null)
chunkBuffer.release();
chunkBuffer = null;
headerBuffer = Retainable.release(headerBuffer);
chunkBuffer = Retainable.release(chunkBuffer);
contentByteBuffer = null;
}
}
@ -334,11 +332,16 @@ public class HttpSenderOverHTTP extends HttpSender
}
}
@Override
protected void onFailure(Throwable cause)
{
callback.failed(cause);
}
@Override
protected void onCompleteFailure(Throwable cause)
{
release();
callback.failed(cause);
}
private void release()

View File

@ -703,7 +703,7 @@ public class ConnectionPoolTest
assertThat(connectionPool.toString(), not(nullValue()));
}
private static class ConnectionPoolFactory
public static class ConnectionPoolFactory
{
private final String name;
private final ConnectionPool.Factory factory;

View File

@ -1865,7 +1865,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest
}
});
// Close the parser to cause the issue.
org.eclipse.jetty.server.HttpConnection.getCurrentConnection().getParser().close();
org.eclipse.jetty.server.internal.HttpConnection.getCurrentConnection().getParser().close();
}
});
server.start();

View File

@ -332,7 +332,10 @@ public class NetworkTrafficListenerTest
@Override
public boolean handle(Request request, Response response, Callback callback)
{
Response.sendRedirect(request, response, callback, location);
Content.Source.consumeAll(request, Callback.from(
() -> Response.sendRedirect(request, response, callback, location),
callback::failed
));
return true;
}
});

View File

@ -31,12 +31,11 @@ public class HttpClientJMXTest
@Test
public void testHttpClientName() throws Exception
{
String name = "foo";
HttpClient httpClient = new HttpClient();
httpClient.setName(name);
try
try (HttpClient httpClient = new HttpClient())
{
String name = "foo";
httpClient.setName(name);
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
MBeanContainer mbeanContainer = new MBeanContainer(mbeanServer);
// Adding MBeanContainer as a bean will trigger the registration of MBeans.
@ -59,9 +58,5 @@ public class HttpClientJMXTest
assertEquals(name, oName.getKeyProperty("context"));
}
}
finally
{
httpClient.stop();
}
}
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<!-- =============================================================== -->
<!-- Attach the "core" environment app deployment provider -->

View File

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<!-- =============================================================== -->
<!-- Create the deployment manager -->

View File

@ -385,6 +385,10 @@ public class ContextProvider extends ScanningAppProvider
// Handle a context XML file
if (FileID.isXml(path))
{
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
if (coreContextClassLoader != null)
Thread.currentThread().setContextClassLoader(coreContextClassLoader);
context = applyXml(context, path, env, properties);
// Look for the contextHandler itself
@ -401,13 +405,11 @@ public class ContextProvider extends ScanningAppProvider
throw new IllegalStateException("Unknown context type of " + context);
// Set the classloader if we have a coreContextClassLoader
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
if (coreContextClassLoader != null)
contextHandler.setClassLoader(coreContextClassLoader);
return contextHandler;
}
// Otherwise it must be a directory or an archive
else if (!Files.isDirectory(path) && !FileID.isWebArchive(path))
{

View File

@ -12,7 +12,7 @@
<!-- // ======================================================================== -->
<!-- // -->
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/global</Set>
<Call name="setAttribute">

View File

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addBean">

View File

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_9_3.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_9_3.dtd">
<!-- ============================================================= -->
<!-- Configure the Jetty Server instance with an ID "Server" -->

View File

@ -1,9 +1,9 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_9_3.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_9_3.dtd">
<!-- =============================================================== -->
<!-- Documentation of this file format can be found at: -->
<!-- https://eclipse.dev/jetty/documentation/ -->
<!-- https://jetty.org/docs/ -->
<!-- -->
<!-- Additional configuration files are available in $JETTY_HOME/etc -->
<!-- and can be mixed in. See start.ini file for the default -->

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_9_3.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.ee9.webapp.WebAppContext">
<Set name="contextPath">/badapp</Set>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/bar</Set>
<Set name="baseResourceAsString"><Property name="test.bar.resourceBase.alt" /></Set>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/bar</Set>
<Set name="baseResourceAsString"><Property name="test.bar.resourceBase" /></Set>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.ee9.webapp.WebAppContext">
<Set name="contextPath">/foo</Set>
<Set name="war">

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/simple</Set>
<Set name="tempDirectory"><Property name="test.tmpBase" /></Set>

View File

@ -1,4 +1,4 @@
# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/
[description]
# tag::description[]

View File

@ -119,11 +119,25 @@ public class HttpChannelOverFCGI extends HttpChannel
receiver.content(chunk);
}
protected void responseContentAvailable()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.responseContentAvailable(exchange);
}
protected void end()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.end(exchange);
receiver.end();
}
protected void responseSuccess()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.responseSuccess(exchange);
}
protected void responseFailure(Throwable failure, Promise<Boolean> promise)

View File

@ -67,7 +67,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
private final HttpChannelOverFCGI channel;
private RetainableByteBuffer networkBuffer;
private Object attachment;
private Runnable action;
private State state = State.STATUS;
private long idleTimeout;
private boolean shutdown;
@ -101,6 +101,12 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
return delegate.getRemoteSocketAddress();
}
@Override
public EndPoint.SslSessionData getSslSessionData()
{
return delegate.getSslSessionData();
}
protected Flusher getFlusher()
{
return flusher;
@ -162,7 +168,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
this.networkBuffer = null;
}
boolean parseAndFill()
boolean parseAndFill(boolean notifyContentAvailable)
{
if (LOG.isDebugEnabled())
LOG.debug("parseAndFill {}", networkBuffer);
@ -173,7 +179,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{
while (true)
{
if (parse(networkBuffer.getByteBuffer()))
if (parse(networkBuffer.getByteBuffer(), notifyContentAvailable))
return false;
if (networkBuffer.isRetained())
@ -208,13 +214,35 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
}
}
private boolean parse(ByteBuffer buffer)
private boolean parse(ByteBuffer buffer, boolean notifyContentAvailable)
{
boolean parse = parser.parse(buffer);
Runnable action = getAndSetAction(null);
if (action != null)
action.run();
return parse;
boolean handle = parser.parse(buffer);
switch (state)
{
case STATUS ->
{
// Nothing to do.
}
case HEADERS -> channel.responseHeaders();
case CONTENT ->
{
if (notifyContentAvailable)
channel.responseContentAvailable();
}
case COMPLETE ->
{
// For the complete event, handle==false, and cannot
// differentiate between a complete event and a parse()
// with zero or not enough bytes, so the state is reset
// here to avoid calling responseSuccess() again.
state = State.STATUS;
channel.responseSuccess();
}
default -> throw new IllegalStateException("Invalid state " + state);
}
return handle;
}
private void shutdown()
@ -312,13 +340,6 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
}, x -> close(failure)));
}
private Runnable getAndSetAction(Runnable action)
{
Runnable r = this.action;
this.action = action;
return r;
}
protected HttpChannelOverFCGI newHttpChannel()
{
return new HttpChannelOverFCGI(this);
@ -359,6 +380,12 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
return getEndPoint().getRemoteSocketAddress();
}
@Override
public EndPoint.SslSessionData getSslSessionData()
{
return getEndPoint().getSslSessionData();
}
@Override
public SendFailure send(HttpExchange exchange)
{
@ -402,6 +429,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{
if (LOG.isDebugEnabled())
LOG.debug("onBegin r={},c={},reason={}", request, code, reason);
state = State.STATUS;
channel.responseBegin(code, reason);
}
@ -418,8 +446,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{
if (LOG.isDebugEnabled())
LOG.debug("onHeaders r={} {}", request, networkBuffer);
if (getAndSetAction(channel::responseHeaders) != null)
throw new IllegalStateException();
state = State.HEADERS;
return true;
}
@ -432,13 +459,10 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{
case STD_OUT ->
{
// No need to call networkBuffer.retain() here, since we know
// that the action will be run before releasing the networkBuffer.
// The receiver of the chunk decides whether to consume/retain it.
Content.Chunk chunk = Content.Chunk.asChunk(buffer, false, networkBuffer);
if (getAndSetAction(() -> channel.content(chunk)) == null)
return true;
throw new IllegalStateException();
channel.content(chunk);
state = State.CONTENT;
return true;
}
case STD_ERR -> LOG.info(BufferUtil.toUTF8String(buffer));
default -> throw new IllegalArgumentException();
@ -452,6 +476,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
if (LOG.isDebugEnabled())
LOG.debug("onEnd r={}", request);
channel.end();
state = State.COMPLETE;
}
@Override
@ -462,4 +487,9 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
failAndClose(failure);
}
}
private enum State
{
STATUS, HEADERS, CONTENT, COMPLETE
}
}

View File

@ -34,13 +34,15 @@ public class HttpReceiverOverFCGI extends HttpReceiver
if (!hasContent())
{
HttpConnectionOverFCGI httpConnection = getHttpChannel().getHttpConnection();
boolean setFillInterest = httpConnection.parseAndFill();
boolean setFillInterest = httpConnection.parseAndFill(true);
if (!hasContent() && setFillInterest)
httpConnection.fillInterested();
}
else
{
responseContentAvailable();
HttpExchange exchange = getHttpExchange();
if (exchange != null)
responseContentAvailable(exchange);
}
}
@ -79,7 +81,7 @@ public class HttpReceiverOverFCGI extends HttpReceiver
if (chunk != null)
return chunk;
HttpConnectionOverFCGI httpConnection = getHttpChannel().getHttpConnection();
boolean needFillInterest = httpConnection.parseAndFill();
boolean needFillInterest = httpConnection.parseAndFill(false);
chunk = consumeChunk();
if (chunk != null)
return chunk;
@ -112,15 +114,18 @@ public class HttpReceiverOverFCGI extends HttpReceiver
// Retain the chunk because it is stored for later reads.
chunk.retain();
this.chunk = chunk;
responseContentAvailable();
}
void end(HttpExchange exchange)
void end()
{
if (chunk != null)
throw new IllegalStateException();
chunk = Content.Chunk.EOF;
responseSuccess(exchange, this::receiveNext);
}
void responseSuccess(HttpExchange exchange)
{
super.responseSuccess(exchange, this::receiveNext);
}
private void receiveNext()
@ -131,7 +136,7 @@ public class HttpReceiverOverFCGI extends HttpReceiver
throw new IllegalStateException();
HttpConnectionOverFCGI httpConnection = getHttpChannel().getHttpConnection();
boolean setFillInterest = httpConnection.parseAndFill();
boolean setFillInterest = httpConnection.parseAndFill(true);
if (!hasContent() && setFillInterest)
httpConnection.fillInterested();
}
@ -160,6 +165,12 @@ public class HttpReceiverOverFCGI extends HttpReceiver
super.responseHeaders(exchange);
}
@Override
protected void responseContentAvailable(HttpExchange exchange)
{
super.responseContentAvailable(exchange);
}
@Override
protected void responseFailure(Throwable failure, Promise<Boolean> promise)
{

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.fcgi.generator;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
@ -104,43 +105,60 @@ public class Flusher
protected void onSuccess()
{
if (active != null)
{
active.release();
active.succeeded();
active = null;
active = null;
}
}
@Override
public void onCompleteFailure(Throwable x)
public void onFailure(Throwable cause)
{
if (active != null)
active.failed(x);
active = null;
while (true)
active.failed(cause);
List<Entry> entries;
try (AutoLock ignored = lock.lock())
{
Entry entry = poll();
if (entry == null)
break;
entry.failed(x);
entries = new ArrayList<>(queue);
}
entries.forEach(entry -> entry.failed(cause));
}
@Override
protected void onCompleteFailure(Throwable cause)
{
if (active != null)
{
active.release();
active = null;
}
List<Entry> entries;
try (AutoLock ignored = lock.lock())
{
entries = new ArrayList<>(queue);
queue.clear();
}
entries.forEach(Entry::release);
}
}
private record Entry(ByteBufferPool.Accumulator accumulator, Callback callback) implements Callback
private record Entry(ByteBufferPool.Accumulator accumulator, Callback callback)
{
@Override
public void succeeded()
{
if (accumulator != null)
accumulator.release();
callback.succeeded();
}
@Override
public void failed(Throwable x)
{
callback.failed(x);
}
private void release()
{
if (accumulator != null)
accumulator.release();
callback.failed(x);
}
}
}

View File

@ -15,8 +15,6 @@ package org.eclipse.jetty.fcgi.parser;
import java.io.EOFException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpCompliance;
@ -43,13 +41,12 @@ public class ResponseContentParser extends StreamContentParser
{
private static final Logger LOG = LoggerFactory.getLogger(ResponseContentParser.class);
private final Map<Integer, ResponseParser> parsers = new ConcurrentHashMap<>();
private final ClientParser.Listener listener;
private final ResponseParser parser;
public ResponseContentParser(HeaderParser headerParser, ClientParser.Listener listener)
{
super(headerParser, FCGI.StreamType.STD_OUT, listener);
this.listener = listener;
this.parser = new ResponseParser(listener);
}
@Override
@ -63,13 +60,6 @@ public class ResponseContentParser extends StreamContentParser
@Override
protected boolean onContent(ByteBuffer buffer)
{
int request = getRequest();
ResponseParser parser = parsers.get(request);
if (parser == null)
{
parser = new ResponseParser(listener, request);
parsers.put(request, parser);
}
return parser.parse(buffer);
}
@ -77,37 +67,44 @@ public class ResponseContentParser extends StreamContentParser
protected void end(int request)
{
super.end(request);
parsers.remove(request);
parser.reset();
}
private static class ResponseParser implements HttpParser.ResponseHandler
private class ResponseParser implements HttpParser.ResponseHandler
{
private final HttpFields.Mutable fields = HttpFields.build();
private final ClientParser.Listener listener;
private final int request;
private final FCGIHttpParser httpParser;
private State state = State.HEADERS;
private boolean seenResponseCode;
private boolean stalled;
private ResponseParser(ClientParser.Listener listener, int request)
private ResponseParser(ClientParser.Listener listener)
{
this.listener = listener;
this.request = request;
this.httpParser = new FCGIHttpParser(this);
}
private void reset()
{
fields.clear();
httpParser.reset();
state = State.HEADERS;
seenResponseCode = false;
stalled = false;
}
public boolean parse(ByteBuffer buffer)
{
int remaining = buffer.remaining();
while (remaining > 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Response {} {}, state {} {}", request, FCGI.StreamType.STD_OUT, state, BufferUtil.toDetailString(buffer));
LOG.debug("Response {} {}, state {} {}", getRequest(), FCGI.StreamType.STD_OUT, state, BufferUtil.toDetailString(buffer));
switch (state)
{
case HEADERS:
case HEADERS ->
{
if (httpParser.parseNext(buffer))
{
@ -116,40 +113,33 @@ public class ResponseContentParser extends StreamContentParser
return true;
}
remaining = buffer.remaining();
break;
}
case CONTENT_MODE:
case CONTENT_MODE ->
{
// If we have no indication of the content, then
// the HTTP parser will assume there is no content
// and will not parse it even if it is provided,
// so we have to parse it raw ourselves here.
boolean rawContent = fields.size() == 0 ||
(fields.get(HttpHeader.CONTENT_LENGTH) == null &&
fields.get(HttpHeader.TRANSFER_ENCODING) == null);
(fields.get(HttpHeader.CONTENT_LENGTH) == null &&
fields.get(HttpHeader.TRANSFER_ENCODING) == null);
state = rawContent ? State.RAW_CONTENT : State.HTTP_CONTENT;
break;
}
case RAW_CONTENT:
case RAW_CONTENT ->
{
ByteBuffer content = buffer.asReadOnlyBuffer();
buffer.position(buffer.limit());
if (notifyContent(content))
return true;
remaining = 0;
break;
}
case HTTP_CONTENT:
case HTTP_CONTENT ->
{
if (httpParser.parseNext(buffer))
return true;
remaining = buffer.remaining();
break;
}
default:
{
throw new IllegalStateException();
}
default -> throw new IllegalStateException();
}
}
return false;
@ -205,7 +195,7 @@ public class ResponseContentParser extends StreamContentParser
{
try
{
listener.onBegin(request, code, reason);
listener.onBegin(getRequest(), code, reason);
}
catch (Throwable x)
{
@ -218,7 +208,7 @@ public class ResponseContentParser extends StreamContentParser
{
try
{
listener.onHeader(request, httpField);
listener.onHeader(getRequest(), httpField);
}
catch (Throwable x)
{
@ -242,7 +232,7 @@ public class ResponseContentParser extends StreamContentParser
{
try
{
return listener.onHeaders(request);
return listener.onHeaders(getRequest());
}
catch (Throwable x)
{
@ -278,7 +268,7 @@ public class ResponseContentParser extends StreamContentParser
{
try
{
return listener.onContent(request, FCGI.StreamType.STD_OUT, buffer);
return listener.onContent(getRequest(), FCGI.StreamType.STD_OUT, buffer);
}
catch (Throwable x)
{
@ -318,7 +308,7 @@ public class ResponseContentParser extends StreamContentParser
{
try
{
listener.onFailure(request, failure);
listener.onFailure(getRequest(), failure);
}
catch (Throwable x)
{

View File

@ -259,8 +259,7 @@ public class ServerFCGIConnection extends AbstractMetaDataConnection implements
boolean released = inputBuffer.release();
if (LOG.isDebugEnabled())
LOG.debug("releaseInputBuffer {} {}", released, this);
if (released)
inputBuffer = null;
inputBuffer = null;
}
private int fillInputBuffer()

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.fcgi.server;
import java.util.concurrent.TimeUnit;
import org.awaitility.Awaitility;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI;
@ -29,10 +28,11 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public abstract class AbstractHttpClientServerTest
{
@ -75,9 +75,9 @@ public abstract class AbstractHttpClientServerTest
try
{
if (serverBufferPool != null)
assertNoLeaks(serverBufferPool, "\n---\nServer Leaks: " + serverBufferPool.dumpLeaks() + "---\n");
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat("Server Leaks: " + serverBufferPool.dumpLeaks(), serverBufferPool.getLeaks().size(), is(0)));
if (clientBufferPool != null)
assertNoLeaks(clientBufferPool, "\n---\nClient Leaks: " + clientBufferPool.dumpLeaks() + "---\n");
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat("Client Leaks: " + clientBufferPool.dumpLeaks(), clientBufferPool.getLeaks().size(), is(0)));
}
finally
{
@ -85,16 +85,4 @@ public abstract class AbstractHttpClientServerTest
LifeCycle.stop(server);
}
}
private void assertNoLeaks(ArrayByteBufferPool.Tracking bufferPool, String msg)
{
try
{
Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() -> bufferPool.getLeaks().size(), Matchers.is(0));
}
catch (Exception e)
{
fail(e.getMessage() + msg);
}
}
}

View File

@ -699,7 +699,7 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
* @return the value of the field as a {@code long},
* or -1 if no such field is present
* @throws NumberFormatException if the value of the field
* cannot be converted to a {@link long}
* cannot be converted to a {@code long}
*/
default long getLongField(String name) throws NumberFormatException
{
@ -715,7 +715,7 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
* @return the value of the field as a {@code long},
* or -1 if no such field is present
* @throws NumberFormatException if the value of the field
* cannot be converted to a {@link long}
* cannot be converted to a {@code long}
*/
default long getLongField(HttpHeader header) throws NumberFormatException
{

View File

@ -455,12 +455,18 @@ public class HttpGenerator
}
}
public void servletUpgrade()
public void startTunnel()
{
_noContentResponse = false;
_state = State.COMMITTED;
}
@Deprecated(since = "12.1.0", forRemoval = true)
public void servletUpgrade()
{
startTunnel();
}
private void prepareChunk(ByteBuffer chunk, int remaining)
{
// if we need CRLF add this to header

View File

@ -2016,11 +2016,17 @@ public class HttpParser
_headerComplete = false;
}
public void startTunnel()
{
setState(State.EOF_CONTENT);
_endOfContent = EndOfContent.EOF_CONTENT;
_contentLength = -1;
}
@Deprecated(since = "12.1.0", forRemoval = true)
public void servletUpgrade()
{
setState(State.CONTENT);
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_contentLength = -1;
startTunnel();
}
protected void setState(State state)

View File

@ -23,6 +23,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
/**
@ -62,7 +63,7 @@ public class HttpTester
public Input()
{
this(BufferUtil.allocate(8192));
this(BufferUtil.allocate(IO.DEFAULT_BUFFER_SIZE));
}
Input(ByteBuffer buffer)
@ -477,7 +478,7 @@ public class HttpTester
switch (result)
{
case NEED_HEADER:
header = BufferUtil.allocate(8192);
header = BufferUtil.allocate(IO.DEFAULT_BUFFER_SIZE);
continue;
case HEADER_OVERFLOW:
@ -491,7 +492,7 @@ public class HttpTester
continue;
case NEED_CHUNK_TRAILER:
chunk = BufferUtil.allocate(8192);
chunk = BufferUtil.allocate(IO.DEFAULT_BUFFER_SIZE);
continue;
case NEED_INFO:

View File

@ -564,7 +564,7 @@ public class MultiPart
public abstract static class AbstractContentSource implements Content.Source, Closeable
{
private final AutoLock lock = new AutoLock();
private final SerializedInvoker invoker = new SerializedInvoker();
private final SerializedInvoker invoker = new SerializedInvoker(AbstractContentSource.class);
private final Queue<Part> parts = new ArrayDeque<>();
private final String boundary;
private final ByteBuffer firstBoundary;

View File

@ -222,7 +222,7 @@ public class MultiPartByteRanges
{
private final Resource resource;
private final ByteRange byteRange;
private final ByteBufferPool bufferPool;
private final ByteBufferPool.Sized bufferPool;
public Part(String contentType, Resource resource, ByteRange byteRange, long contentLength)
{
@ -233,7 +233,7 @@ public class MultiPartByteRanges
public Part(String contentType, Resource resource, ByteRange byteRange, long contentLength, ByteBufferPool bufferPool)
{
this(HttpFields.build().put(HttpHeader.CONTENT_TYPE, contentType)
.put(HttpHeader.CONTENT_RANGE, byteRange.toHeaderValue(contentLength)), resource, byteRange, bufferPool);
.put(HttpHeader.CONTENT_RANGE, byteRange.toHeaderValue(contentLength)), resource, byteRange, new ByteBufferPool.Sized(bufferPool));
}
public Part(HttpFields headers, Resource resource, ByteRange byteRange)
@ -241,18 +241,18 @@ public class MultiPartByteRanges
this(headers, resource, byteRange, null);
}
public Part(HttpFields headers, Resource resource, ByteRange byteRange, ByteBufferPool bufferPool)
public Part(HttpFields headers, Resource resource, ByteRange byteRange, ByteBufferPool.Sized bufferPool)
{
super(null, null, headers);
this.resource = resource;
this.byteRange = byteRange;
this.bufferPool = bufferPool == null ? ByteBufferPool.NON_POOLING : bufferPool;
this.bufferPool = bufferPool == null ? ByteBufferPool.SIZED_NON_POOLING : bufferPool;
}
@Override
public Content.Source newContentSource()
{
return IOResources.asContentSource(resource, bufferPool, 0, false, byteRange.first(), byteRange.getLength());
return IOResources.asContentSource(resource, bufferPool, byteRange.first(), byteRange.getLength());
}
}

View File

@ -69,16 +69,15 @@ public class CachingHttpContentFactory implements HttpContent.Factory
private final HttpContent.Factory _authority;
private final ConcurrentHashMap<String, CachingHttpContent> _cache = new ConcurrentHashMap<>();
private final AtomicLong _cachedSize = new AtomicLong();
private final ByteBufferPool _bufferPool;
private final ByteBufferPool.Sized _bufferPool;
private int _maxCachedFileSize = DEFAULT_MAX_CACHED_FILE_SIZE;
private int _maxCachedFiles = DEFAULT_MAX_CACHED_FILES;
private long _maxCacheSize = DEFAULT_MAX_CACHE_SIZE;
private boolean _useDirectByteBuffers = true;
public CachingHttpContentFactory(HttpContent.Factory authority, ByteBufferPool bufferPool)
public CachingHttpContentFactory(HttpContent.Factory authority, ByteBufferPool.Sized bufferPool)
{
_authority = authority;
_bufferPool = bufferPool != null ? bufferPool : ByteBufferPool.NON_POOLING;
_bufferPool = bufferPool != null ? bufferPool : ByteBufferPool.SIZED_NON_POOLING;
}
protected ConcurrentMap<String, CachingHttpContent> getCache()
@ -137,16 +136,6 @@ public class CachingHttpContentFactory implements HttpContent.Factory
shrinkCache();
}
public boolean isUseDirectByteBuffers()
{
return _useDirectByteBuffers;
}
public void setUseDirectByteBuffers(boolean useDirectByteBuffers)
{
_useDirectByteBuffers = useDirectByteBuffers;
}
private void shrinkCache()
{
// While we need to shrink
@ -334,7 +323,7 @@ public class CachingHttpContentFactory implements HttpContent.Factory
throw new IllegalArgumentException("Resource is too large: length " + contentLengthValue + " > " + _maxCachedFileSize);
// Read the content into memory
_buffer = IOResources.toRetainableByteBuffer(httpContent.getResource(), _bufferPool, _useDirectByteBuffers);
_buffer = IOResources.toRetainableByteBuffer(httpContent.getResource(), _bufferPool);
_characterEncoding = httpContent.getCharacterEncoding();
_compressedFormats = httpContent.getPreCompressedContentFormats();

View File

@ -121,7 +121,7 @@ public class ResourceHttpContent implements HttpContent
@Override
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
{
IOResources.copy(_resource, sink, _sizedBufferPool, _sizedBufferPool.getSize(), _sizedBufferPool.isDirect(), offset, length, callback);
IOResources.copy(_resource, sink, _sizedBufferPool, offset, length, callback);
}
@Override

View File

@ -59,7 +59,7 @@ public class ValidatingCachingHttpContentFactory extends CachingHttpContentFacto
*/
public ValidatingCachingHttpContentFactory(@Name("authority") HttpContent.Factory authority,
@Name("validationPeriod") long validationPeriod,
@Name("bufferPool") ByteBufferPool bufferPool)
@Name("bufferPool") ByteBufferPool.Sized bufferPool)
{
this(authority, validationPeriod, bufferPool, null, -1, -1);
}
@ -77,7 +77,7 @@ public class ValidatingCachingHttpContentFactory extends CachingHttpContentFacto
*/
public ValidatingCachingHttpContentFactory(@Name("authority") HttpContent.Factory authority,
@Name("validationPeriod") long validationPeriod,
@Name("byteBufferPool") ByteBufferPool bufferPool,
@Name("byteBufferPool") ByteBufferPool.Sized bufferPool,
@Name("scheduler") Scheduler scheduler,
@Name("sweepPeriod") long sweepPeriod,
@Name("idleTimeout") long idleTimeout)

View File

@ -0,0 +1,16 @@
[description]
Adds the Jetty HTTP/2 client transport dependencies to the server classpath.
[tags]
client
http2
[depends]
client
http2-client
[files]
maven://org.eclipse.jetty.http2/jetty-http2-client-transport/${jetty.version}/jar|lib/http2/jetty-http2-client-transport-${jetty.version}.jar
[lib]
lib/http2/jetty-http2-client-transport-${jetty.version}.jar

View File

@ -124,12 +124,6 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport
connect(address, destination.getClientConnectionFactory(), listenerPromise, listenerPromise, context);
}
@Override
public void connect(InetSocketAddress address, Map<String, Object> context)
{
connect((SocketAddress)address, context);
}
protected void connect(SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise<Session> promise, Map<String, Object> context)
{
HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);

View File

@ -44,6 +44,7 @@ import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.Sweeper;
import org.slf4j.Logger;
@ -85,6 +86,12 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S
return session.getRemoteSocketAddress();
}
@Override
public EndPoint.SslSessionData getSslSessionData()
{
return connection.getEndPoint().getSslSessionData();
}
public boolean isRecycleHttpChannels()
{
return recycleHttpChannels;

View File

@ -50,8 +50,6 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
{
private static final Logger LOG = LoggerFactory.getLogger(HttpReceiverOverHTTP2.class);
private final Runnable onDataAvailableTask = new Invocable.ReadyTask(Invocable.InvocationType.NON_BLOCKING, this::responseContentAvailable);
public HttpReceiverOverHTTP2(HttpChannel channel)
{
super(channel);
@ -70,6 +68,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
Stream stream = getHttpChannel().getStream();
if (stream == null)
return Content.Chunk.from(new EOFException("Channel has been released"));
Stream.Data data = stream.readData();
if (LOG.isDebugEnabled())
LOG.debug("Read stream data {} in {}", data, this);
@ -79,13 +78,25 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
stream.demand();
return null;
}
DataFrame frame = data.frame();
boolean last = frame.remaining() == 0 && frame.isEndStream();
if (!last)
return Content.Chunk.asChunk(frame.getByteBuffer(), last, data);
return Content.Chunk.asChunk(frame.getByteBuffer(), false, data);
data.release();
responseSuccess(getHttpExchange(), null);
return Content.Chunk.EOF;
if (stream.isReset())
{
Throwable failure = new EOFException("Stream has been reset");
responseFailure(failure, Promise.noop());
return Content.Chunk.from(failure);
}
else
{
responseSuccess(getHttpExchange(), null);
return Content.Chunk.EOF;
}
}
@Override
@ -213,7 +224,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return null;
return onDataAvailableTask;
return new Invocable.ReadyTask(Invocable.InvocationType.NON_BLOCKING, () -> responseContentAvailable(exchange));
}
@Override

View File

@ -0,0 +1,18 @@
[description]
Adds the Jetty HTTP/2 client dependencies to the server classpath.
[tags]
client
http2
[files]
maven://org.eclipse.jetty/jetty-alpn-client/${jetty.version}/jar|lib/jetty-alpn-client-${jetty.version}.jar
maven://org.eclipse.jetty/jetty-alpn-java-client/${jetty.version}/jar|lib/jetty-alpn-java-client-${jetty.version}.jar
maven://org.eclipse.jetty.http2/jetty-http2-client/${jetty.version}/jar|lib/http2/jetty-http2-client-${jetty.version}.jar
[lib]
lib/jetty-alpn-client-${jetty.version}.jar
lib/jetty-alpn-java-client-${jetty.version}.jar
lib/http2/jetty-http2-client-${jetty.version}.jar
lib/http2/jetty-http2-common-${jetty.version}.jar
lib/http2/jetty-http2-hpack-${jetty.version}.jar

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Transport;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
@ -99,10 +100,10 @@ import org.eclipse.jetty.util.thread.Scheduler;
*} </pre>
*/
@ManagedObject
public class HTTP2Client extends ContainerLifeCycle
public class HTTP2Client extends ContainerLifeCycle implements AutoCloseable
{
private final ClientConnector connector;
private int inputBufferSize = 8192;
private int inputBufferSize = IO.DEFAULT_BUFFER_SIZE;
private List<String> protocols = List.of("h2");
private int initialSessionRecvWindow = 16 * 1024 * 1024;
private int initialStreamRecvWindow = 8 * 1024 * 1024;
@ -492,4 +493,10 @@ public class HTTP2Client extends ContainerLifeCycle
}
return factory;
}
@Override
public void close() throws Exception
{
stop();
}
}

View File

@ -95,8 +95,7 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
public void onOpen()
{
Map<Integer, Integer> settings = listener.onPreface(getSession());
if (settings == null)
settings = new HashMap<>();
settings = settings == null ? new HashMap<>() : new HashMap<>(settings);
// Below we want to populate any settings to send to the server
// that have a different default than what prescribed by the RFC.

View File

@ -1248,6 +1248,11 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
this.stream = stream;
}
public Frame frame()
{
return frame;
}
public abstract int getFrameBytesGenerated();
public int getDataBytesRemaining()

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.http2;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
@ -54,30 +53,12 @@ public abstract class HTTP2StreamEndPoint implements EndPoint
this.stream = stream;
}
@Override
public InetSocketAddress getLocalAddress()
{
SocketAddress local = getLocalSocketAddress();
if (local instanceof InetSocketAddress)
return (InetSocketAddress)local;
return null;
}
@Override
public SocketAddress getLocalSocketAddress()
{
return stream.getSession().getLocalSocketAddress();
}
@Override
public InetSocketAddress getRemoteAddress()
{
SocketAddress remote = getRemoteSocketAddress();
if (remote instanceof InetSocketAddress)
return (InetSocketAddress)remote;
return null;
}
@Override
public SocketAddress getRemoteSocketAddress()
{

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.http2.internal;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@ -24,10 +25,12 @@ import java.util.List;
import java.util.Queue;
import java.util.Set;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.HTTP2Connection;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.HTTP2Stream;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.EndPoint;
@ -51,7 +54,6 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
private final Collection<HTTP2Session.Entry> processedEntries = new ArrayList<>();
private final HTTP2Session session;
private final RetainableByteBuffer.Mutable accumulator;
private boolean released;
private InvocationType invocationType = InvocationType.NON_BLOCKING;
private Throwable terminated;
private HTTP2Session.Entry stalledEntry;
@ -95,10 +97,9 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
entries.offerFirst(entry);
if (LOG.isDebugEnabled())
LOG.debug("Prepended {}, entries={}", entry, entries.size());
return true;
}
}
if (closed == null)
return true;
closed(entry, closed);
return false;
}
@ -109,15 +110,17 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
try (AutoLock ignored = lock.lock())
{
closed = terminated;
// If it was not possible to HPACK encode, then allow to send a GOAWAY.
if (closed instanceof HpackException.SessionException && entry.frame().getType() == FrameType.GO_AWAY)
closed = null;
if (closed == null)
{
entries.offer(entry);
if (LOG.isDebugEnabled())
LOG.debug("Appended {}, entries={}, {}", entry, entries.size(), this);
return true;
}
}
if (closed == null)
return true;
closed(entry, closed);
return false;
}
@ -133,10 +136,9 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
list.forEach(entries::offer);
if (LOG.isDebugEnabled())
LOG.debug("Appended {}, entries={} {}", list, entries.size(), this);
return true;
}
}
if (closed == null)
return true;
list.forEach(entry -> closed(entry, closed));
return false;
}
@ -166,7 +168,21 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
try (AutoLock ignored = lock.lock())
{
if (terminated != null)
throw terminated;
{
boolean rethrow = true;
if (terminated instanceof HpackException.SessionException)
{
HTTP2Session.Entry entry = entries.peek();
if (entry != null && entry.frame().getType() == FrameType.GO_AWAY)
{
// Allow a SessionException to be processed once to send a GOAWAY.
terminated = new ClosedChannelException().initCause(terminated);
rethrow = false;
}
}
if (rethrow)
throw terminated;
}
WindowEntry windowEntry;
while ((windowEntry = windows.poll()) != null)
@ -251,6 +267,15 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
entry.failed(failure);
pending.remove();
}
catch (HpackException.SessionException failure)
{
if (LOG.isDebugEnabled())
LOG.debug("Failure generating {}", entry, failure);
onSessionFailure(failure);
// The method above will try to send
// a GOAWAY, so we will iterate again.
return Action.IDLE;
}
catch (Throwable failure)
{
// Failure to generate the entry is catastrophic.
@ -308,7 +333,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
private void finish()
{
release();
accumulator.clear();
processedEntries.forEach(HTTP2Session.Entry::succeeded);
processedEntries.clear();
invocationType = InvocationType.NON_BLOCKING;
@ -328,26 +353,38 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
}
}
private void release()
{
if (!released)
{
released = true;
accumulator.release();
}
}
@Override
protected void onCompleteSuccess()
{
throw new IllegalStateException();
}
@Override
protected void onFailure(Throwable x)
{
Throwable closed = fail(x);
// If the failure came from within the
// flusher, we need to close the connection.
if (closed == null)
session.onWriteFailure(x);
}
@Override
protected void onCompleteFailure(Throwable x)
{
release();
accumulator.release();
}
private void onSessionFailure(Throwable x)
{
accumulator.clear();
Throwable closed = fail(x);
if (closed == null)
session.close(ErrorCode.COMPRESSION_ERROR.code, null, NOOP);
}
private Throwable fail(Throwable x)
{
Throwable closed;
Set<HTTP2Session.Entry> allEntries;
try (AutoLock ignored = lock.lock())
@ -369,11 +406,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
allEntries.addAll(pendingEntries);
pendingEntries.clear();
allEntries.forEach(entry -> entry.failed(x));
// If the failure came from within the
// flusher, we need to close the connection.
if (closed == null)
session.onWriteFailure(x);
return closed;
}
public void terminate(Throwable cause)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
<Call name="addConnectionFactory">

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
<Configure id="httpConnector" class="org.eclipse.jetty.server.ServerConnector">
<Call name="addConnectionFactory">

View File

@ -72,34 +72,40 @@
--add-reads org.eclipse.jetty.http2.server=org.eclipse.jetty.logging</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>h2spec-maven-plugin</artifactId>
<version>1.0.12</version>
<configuration>
<mainClass>org.eclipse.jetty.http2.tests.H2SpecServer</mainClass>
<skip>${h2spec.skip}</skip>
<junitPackage>org.eclipse.jetty.h2spec</junitPackage>
<skipNoDockerAvailable>true</skipNoDockerAvailable>
<reportsDirectory>${project.build.directory}/h2spec-reports</reportsDirectory>
<excludeSpecs>
<excludeSpec>3.5 - Sends invalid connection preface</excludeSpec>
</excludeSpecs>
</configuration>
<executions>
<execution>
<id>h2spec</id>
<goals>
<goal>h2spec</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>h2spec</id>
<build>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>h2spec-maven-plugin</artifactId>
<version>1.0.13</version>
<configuration>
<mainClass>org.eclipse.jetty.http2.tests.H2SpecServer</mainClass>
<skip>${h2spec.skip}</skip>
<junitPackage>org.eclipse.jetty.h2spec</junitPackage>
<skipNoDockerAvailable>true</skipNoDockerAvailable>
<excludeSpecs>
<excludeSpec>3.5 - Sends invalid connection preface</excludeSpec>
</excludeSpecs>
</configuration>
<executions>
<execution>
<id>h2spec</id>
<goals>
<goal>h2spec</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>run-spec-server</id>
<build>

View File

@ -122,7 +122,7 @@ public class BadURITest
Thread.sleep(1000);
// Send a second request and verify that it hits the Handler.
accumulator.release();
accumulator.clear();
MetaData.Request metaData2 = new MetaData.Request(
HttpMethod.GET.asString(),
HttpScheme.HTTP.asString(),

Some files were not shown because too many files have changed in this diff Show More