diff --git a/jetty-documentation/pom.xml b/jetty-documentation/pom.xml index 1a2f42bf4ec..d7aadabe49e 100644 --- a/jetty-documentation/pom.xml +++ b/jetty-documentation/pom.xml @@ -49,6 +49,11 @@ + + org.eclipse.jetty + jetty-client + ${project.version} + org.eclipse.jetty jetty-io @@ -111,6 +116,13 @@ org.asciidoctor asciidoctor-maven-plugin ${asciidoctor.maven.plugin.version} + + + org.asciidoctor + asciidoctorj-diagram + 2.0.1 + + http://www.eclipse.org/jetty/javadoc/${javadoc.version} @@ -183,6 +195,10 @@ ${basedir}/src/main/asciidoc/embedded-guide index.adoc ${project.build.directory}/html/embedded-guide + coderay + + asciidoctor-diagram + diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/client-concepts.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/client-io-arch.adoc similarity index 54% rename from jetty-documentation/src/main/asciidoc/embedded-guide/client/client-concepts.adoc rename to jetty-documentation/src/main/asciidoc/embedded-guide/client/client-io-arch.adoc index bf63bce5013..f2afffd946e 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/client-concepts.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/client-io-arch.adoc @@ -16,32 +16,25 @@ // ======================================================================== // -[[client-concepts]] -=== Client Libraries Concepts +[[client-io-arch]] +=== Client Libraries Architecture -The Jetty client libraries implement a network client speaking different protocols -such as HTTP/1.1, HTTP/2, WebSocket and FastCGI. +The Jetty client libraries provide the basic components and APIs to implement +a network client. -It is possible to implement your own custom protocol on top of the Jetty client -libraries. +They build on the common link:#io-arch[Jetty I/O Architecture] and provide client +specific concepts (such as establishing a connection to a server). -NOTE: TODO: perhaps add a section about this. +There are conceptually two layers that compose the Jetty client libraries: -There are conceptually three layers that compose the Jetty client libraries, from -more abstract to more concrete: +. link:#client-io-arch-network[The network layer], that handles the low level +I/O and deals with buffers, threads, etc. +. link:#client-io-arch-protocol[The protocol layer], that handles the parsing +of bytes read from the network and the generation of bytes to write to the +network. -. The API layer, that exposes semantic APIs to applications so that they can write -code such as "GET me the resource at this URI". -. The protocol layer, where the API request is converted into the appropriate -protocol bytes, for example encrypted HTTP/2. -. The infrastructure layer, that handles the low level I/O and deals with network, -buffer, threads, etc. - -Let's look at these layers starting from the more concrete (and low level) one -and build up to the more abstract layer. - -[[client-concepts-infrastructure]] -==== Client Libraries Infrastructure Layer +[[client-io-arch-network]] +==== Client Libraries Network Layer The Jetty client libraries use the common I/O design described in link:#io-arch[this section]. @@ -78,7 +71,7 @@ include::../{doc_code}/embedded/client/ClientConnectorDocs.java[tags=typical] ---- A more advanced example that customizes the `ClientConnector` by overriding -factory methods: +some of its methods: [source,java,indent=0] ---- @@ -86,7 +79,7 @@ include::../{doc_code}/embedded/client/ClientConnectorDocs.java[tags=advanced] ---- Since `ClientConnector` is the component that handles the low-level network, it -is also the component where you want to configure the low-leven network configuration. +is also the component where you want to configure the low-level network configuration. The most common parameters are: @@ -122,28 +115,82 @@ Please refer to the `ClientConnector` link:{JDURL}/org/eclipse/jetty/io/ClientConnector.html[javadocs] for the complete list of configurable parameters. -Once the `ClientConnector` is configured and started, it can be used to connect -to the server via `ClientConnector.connect(SocketAddress, Map)` -which in turn will call `SocketChannel.connect(SocketAddress)`. +[[client-io-arch-protocol]] +==== Client Libraries Protocol Layer -When establishing a TCP connection to a server, applications need to tell +The protocol layer builds on top of the network layer to generate the +bytes to be written to the network and to parse the bytes read from the +network. + +Recall from link:#io-arch-connection[this section] that Jetty uses the +`Connection` abstraction to produce and interpret the network bytes. + +On the client side, a `ClientConnectionFactory` implementation is the +component that creates `Connection` instances based on the protocol that +the client wants to "speak" with the server. + +Applications use `ClientConnector.connect(SocketAddress, Map)` +to establish a TCP connection to the server, and must tell `ClientConnector` how to create the `Connection` for that particular -TCP connection. -This is done via a -link:{JDURL}/org/eclipse/jetty/io/ClientConnectionFactory.html[`ClientConnectionFactory`]. -that must be passed in the context `Map` as follows: +TCP connection, and how to notify back the application when the connection +creation succeeds or fails. + +This is done by passing a +link:{JDURL}/org/eclipse/jetty/io/ClientConnectionFactory.html[`ClientConnectionFactory`] +(that creates `Connection` instances) and a +link:{JDURL}/org/eclipse/jetty/util/Promise.html[`Promise`] (that is notified +of connection creation success or failure) in the context `Map` as follows: [source,java,indent=0] ---- include::../{doc_code}/embedded/client/ClientConnectorDocs.java[tags=connect] ---- -TODO: expand on what is the API to use, what parameters the context Map must -have, and basically how we can write a generic network client with it. +When a `Connection` is created successfully, its `onOpen()` method is invoked, +and then the promise is completed successfully. -[[client-concepts-protocol]] -==== Client Libraries Protocol Layer +It is now possible to write a super-simple `telnet` client that reads and writes +string lines: -The protocol layer builds on top of the infrastructure layer to generate the -bytes to be written to the network and to parse the bytes received from the -network. +[source,java,indent=0] +---- +include::../{doc_code}/embedded/client/ClientConnectorDocs.java[tags=telnet] +---- + +Note how a very basic "telnet" API that applications could use is implemented +in the form of the `onLine(Consumer)` for the non-blocking receiving +side and `writeLine(String, Callback)` for the non-blocking sending side. +Note also how the `onFillable()` method implements some basic "parsing" +by looking up the `\n` character in the buffer. + +NOTE: The "telnet" client above looks like a super-simple HTTP client because +HTTP/1.0 can be seen as a line-based protocol. HTTP/1.0 was used just as an +example, but we could have used any other line-based protocol such as +link:https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol[SMTP], +provided that the server was able to understand it. + +This is very similar to what the Jetty client implementation does for real +network protocols. +Real network protocols are of course more complicated and so is the implementation +code that handles them, but the general ideas are similar. + +The Jetty client implementation provides a number of `ClientConnectionFactory` +implementations that can be composed to produce and interpret the network bytes. + +For example, it is simple to modify the above example to use the TLS protocol +so that you will be able to connect to the server on port `443`, typically +reserved for the encrypted HTTP protocol. + +The differences between the clear-text version and the TLS encrypted version +are minimal: + +[source,java,indent=0] +---- +include::../{doc_code}/embedded/client/ClientConnectorDocs.java[tags=tlsTelnet] +---- + +The differences with the clear-text version are only: + +* Change the port from `80` to `443`. +* Wrap the `ClientConnectionFactory` with `SslClientConnectionFactory`. +* Unwrap the `SslConnection` to access `TelnetConnection`. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/client.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/client.adoc index 4673ae1e6b5..ac1bd6e9170 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/client.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/client.adoc @@ -17,12 +17,15 @@ // [[client]] -== Jetty Client Libraries +== Client Libraries -The Eclipse Jetty Project provides not only server-side libraries so that you -can embed a server in your code, but it also provides client-side libraries -that allow you to embed a client - for example a HTTP client invoking a third -party HTTP service - in your application. +The Eclipse Jetty Project provides also provides client-side libraries +that allow you to embed a client in your applications. +A typical example is an application that needs to contact a third party +service via HTTP (for example a REST service). +Another example is a proxy application that receives HTTP requests and +forwards them as FCGI requests to a PHP application such as WordPress, +or receives HTTP/1.1 requests and converts them to HTTP/2. The client libraries are designed to be non-blocking and offer both synchronous and asynchronous APIs and come with a large number of configuration options. @@ -32,4 +35,9 @@ There are primarily two client libraries: * link:#client-http[The HTTP client library] * link:#client-websocket[The WebSocket client library] -include::client-concepts.adoc[] +If you are interested in the low-level details of how the Eclipse Jetty +client libraries work, or are interested in writing a custom protocol, +look at the link:#client-io-arch[Client I/O Architecture]. + +include::http/client-http.adoc[] +include::client-io-arch.adoc[] diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc new file mode 100644 index 00000000000..4765fb47a18 --- /dev/null +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc @@ -0,0 +1,297 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +[[client-http-api]] +=== HttpClient API Usage + +`HttpClient` provides two types of APIs: a blocking API and a non-blocking API. + +[[client-http-blocking]] +==== HttpClient Blocking APIs + +The simpler way to perform a HTTP request is the following: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=simpleBlockingGet] +---- + +The method `HttpClient.GET(...)` performs a HTTP `GET` request to the given URI and returns a `ContentResponse` when the request/response conversation completes successfully. + +The `ContentResponse` object contains the HTTP response information: status code, headers and possibly content. +The content length is limited by default to 2 MiB; for larger content see xref:http-client-response-content[]. + +If you want to customize the request, for example by issuing a `HEAD` request instead of a `GET`, and simulating a browser user agent, you can do it in this way: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=headFluent] +---- + +This is a shorthand for: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=headNonFluent] +---- + +You first create a request object using `httpClient.newRequest(...)`, and then you customize it using the fluent API style (that is, a chained invocation of methods on the request object). +When the request object is customized, you call `request.send()` that produces the `ContentResponse` when the request/response conversation is complete. + +Simple `POST` requests also have a shortcut method: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=postFluent] +---- + +The `POST` parameter values added via the `param()` method are automatically URL-encoded. + +Jetty's `HttpClient` automatically follows redirects, so it handles the typical web pattern http://en.wikipedia.org/wiki/Post/Redirect/Get[POST/Redirect/GET], and the response object contains the content of the response of the `GET` request. +Following redirects is a feature that you can enable/disable on a per-request basis or globally. + +File uploads also require one line, and make use of `java.nio.file` classes: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=fileFluent] +---- + +It is possible to impose a total timeout for the request/response conversation using the `Request.timeout(...)` method as follows: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=totalTimeout] +---- + +In the example above, when the 5 seconds expire, the request is aborted and a `java.util.concurrent.TimeoutException` is thrown. + +[[client-http-non-blocking]] +==== HttpClient Non-Blocking APIs + +So far we have shown how to use Jetty HTTP client in a blocking style - that is, the thread that issues the request blocks until the request/response conversation is complete. + +This section will look at Jetty's `HttpClient` non-blocking, asynchronous APIs that are perfectly suited for large content downloads, for parallel processing of requests/responses and in cases where performance and efficient thread and resource utilization is a key factor. + +The asynchronous APIs rely heavily on listeners that are invoked at various stages of request and response processing. +These listeners are implemented by applications and may perform any kind of logic. +The implementation invokes these listeners in the same thread that is used to process the request or response. +Therefore, if the application code in these listeners takes a long time to execute, the request or response processing is delayed until the listener returns. + +If you need to execute application code that takes long time inside a listener, you must spawn your own thread. + +Request and response processing are executed by two different threads and therefore may happen concurrently. +A typical example of this concurrent processing is an echo server, where a large upload may be concurrent with the large download echoed back. +As a side note, remember that responses may be processed and completed _before_ requests; a typical example is a large upload that triggers a quick response - for example an error - by the server: the response may arrive and be completed while the request content is still being uploaded. + +The application thread that calls `Request.send(Response.CompleteListener)` performs the processing of the request until either the request is fully processed or until it would block on I/O, then it returns (and therefore never blocks). +If it would block on I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns. +When such an event is fired, a thread taken from the `HttpClient` thread pool will resume the processing of the request. + +Response are processed from the I/O thread that fires the event that bytes are ready to be read. +Response processing continues until either the response is fully processed or until it would block for I/O. +If it would block for I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns. +When such an event is fired, a thread taken from the `HttpClient` thread pool will resume the processing of the response. + +When the request and the response are both fully processed, the thread that finished the last processing (usually the thread that processes the response, but may also be the thread that processes the request - if the request takes more time than the response to be processed) is used to dequeue the next request for the same destination and processes it. + +A simple non-blocking `GET` request that discards the response content can be written in this way: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=simpleNonBlocking] +---- + +Method `Request.send(Response.CompleteListener)` returns `void` and does not block; the `Response.CompleteListener` lambda provided as a parameter is notified when the request/response conversation is complete, and the `Result` parameter allows you to access the request and response objects as well as failures, if any. + +You can impose a total timeout for the request/response conversation in the same way used by the synchronous API: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=nonBlockingTotalTimeout] +---- + +The example above will impose a total timeout of 3 seconds on the request/response conversation. + +The HTTP client APIs use listeners extensively to provide hooks for all possible request and response events: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=listeners] +---- + +This makes Jetty HTTP client suitable for HTTP load testing because, for example, you can accurately time every step of the request/response conversation (thus knowing where the request/response time is really spent). + +Have a look at the link:{JDURL}/org/eclipse/jetty/client/api/Request.Listener.html[`Request.Listener`] class to know about request events, and to the link:{JDURL}/org/eclipse/jetty/client/api/Response.Listener.html[`Response.Listener`] class to know about response events. + +[[client-http-content]] +==== HttpClient Content Handling + +[[client-http-content-request]] +===== Request Content Handling + +Jetty's `HttpClient` provides a number of utility classes off the shelf to handle request content. + +You can provide request content as `String`, `byte[]`, `ByteBuffer`, `java.nio.file.Path`, `InputStream`, and provide your own implementation of `org.eclipse.jetty.client.api.Request.Content`. +Here’s an example that provides the request content using `java.nio.file.Paths`: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=pathRequestContent] +---- + +Alternatively, you can use `FileInputStream` via the `InputStreamRequestContent` utility class: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=inputStreamRequestContent] +---- + +Since `InputStream` is blocking, then also the send of the request will block if the input stream blocks, even in case of usage of the non-blocking `HttpClient` APIs. + +If you have already read the content in memory, you can pass it as a `byte[]` (or a `String`) using the `BytesRequestContent` (or `StringRequestContent`) utility class: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=bytesStringRequestContent] +---- + +If the request content is not immediately available, but your application will be notified of the content to send, you can use `AsyncRequestContent` in this way: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=asyncRequestContent] +---- + +While the request content is awaited and consequently uploaded by the client application, the server may be able to respond (at least with the response headers) completely asynchronously. +In this case, `Response.Listener` callbacks will be invoked before the request is fully sent. +This allows fine-grained control of the request/response conversation: for example the server may reject contents that are too big, send a response to the client, which in turn may stop the content upload. + +Another way to provide request content is by using an `OutputStreamRequestContent`, +which allows applications to write request content when it is available to the `OutputStream` provided by `OutputStreamRequestContent`: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=outputStreamRequestContent] +---- + +[[http-client-response-content]] +===== Response Content Handling + +Jetty's `HttpClient` allows applications to handle response content in different ways. + +You can buffer the response content in memory; this is done when using the xref:client-http-blocking[blocking APIs] and the content is buffered within a `ContentResponse` up to 2 MiB. + +If you want to control the length of the response content (for example limiting to values smaller than the default of 2 MiB), then you can use a `org.eclipse.jetty.client.util.FutureResponseListener` in this way: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=futureResponseListener] +---- + +If the response content length is exceeded, the response will be aborted, and an exception will be thrown by method `get(...)`. + +You can buffer the response content in memory also using the xref:client-http-non-blocking[non-blocking APIs], via the `BufferingResponseListener` utility class: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=bufferingResponseListener] +---- + +If you want to avoid buffering, you can wait for the response and then stream the content using the `InputStreamResponseListener` utility class: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=inputStreamResponseListener] +---- + +Finally, let's look at the advanced usage of the response content handling. + +The response content is provided by the `HttpClient` implementation to application +listeners following a reactive model similar to that of `java.util.concurrent.Flow`. + +The listener that follows this model is `Response.DemandedContentListener`. + +After the response headers have been processed by the `HttpClient` implementation, +`Response.DemandedContentListener.onBeforeContent(response, demand)` is +invoked. This allows the application to control whether to demand the first +content or not. The default implementation of this method calls `demand.accept(1)`, +which demands one chunk of content to the implementation. +The implementation will deliver the chunk of content as soon as it is available. + +The chunks of content are delivered to the application by invoking +`Response.DemandedContentListener.onContent(response, demand, buffer, callback)`. +Applications implement this method to process the content bytes in the `buffer`. +Succeeding the `callback` signals to the implementation that the application +has consumed the `buffer` so that the implementation can dispose/recycle the +`buffer`. Failing the `callback` signals to the implementation to fail the +response (no more content will be delivered, and the _response failed_ event +will be emitted). + +IMPORTANT: Succeeding the `callback` must be done only after the `buffer` +bytes have been consumed. When the `callback` is succeeded, the `HttpClient` +implementation may reuse the `buffer` and overwrite the bytes with different +bytes; if the application looks at the `buffer` _after_ having succeeded +the `callback` is may see other, unrelated, bytes. + +The application uses the `demand` object to demand more content chunks. +Applications will typically demand for just one more content via +`demand.accept(1)`, but may decide to demand for more via `demand.accept(2)` +or demand "infinitely" once via `demand.accept(Long.MAX_VALUE)`. +Applications that demand for more than 1 chunk of content must be prepared +to receive all the content that they have demanded. + +Demanding for content and consuming the content are orthogonal activities. + +An application can demand "infinitely" and store aside the pairs +`(buffer, callback)` to consume them later. +If not done carefully, this may lead to excessive memory consumption, since +the ``buffer``s are not consumed. +Succeeding the ``callback``s will result in the ``buffer``s to be +disposed/recycled and may be performed at any time. + +An application can also demand one chunk of content, consume it (by +succeeding the associated `callback`) and then _not_ demand for more content +until a later time. + +Subclass `Response.AsyncContentListener` overrides the behavior of +`Response.DemandedContentListener`; when an application implementing its +`onContent(response, buffer, callback)` succeeds the `callback`, it +will have _both_ the effect of disposing/recycling the `buffer` _and_ the +effect of demanding one more chunk of content. + +Subclass `Response.ContentListener` overrides the behavior of +`Response.AsyncContentListener`; when an application implementing its +`onContent(response, buffer)` returns from the method itself, it will +_both_ the effect of disposing/recycling the `buffer` _and_ the effect +of demanding one more chunk of content. + +Previous examples of response content handling were inefficient because they +involved copying the `buffer` bytes, either to accumulate them aside so that +the application could use them when the request was completed, or because +they were provided to an API such as `InputStream` that made use of `byte[]` +(and therefore a copy from `ByteBuffer` to `byte[]` is necessary). + +An application that implements a forwarder between two servers can be +implemented efficiently by handling the response content without copying +the `buffer` bytes as in the following example: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=demandedContentListener] +---- diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-authentication.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc similarity index 97% rename from jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-authentication.adoc rename to jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc index 8b7515f315f..4e3e48704ae 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-authentication.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-authentication.adoc @@ -16,7 +16,7 @@ // ======================================================================== // -[[http-client-authentication]] +[[client-http-authentication]] === Authentication Support Jetty's HTTP client supports the `BASIC` and `DIGEST` authentication mechanisms defined by link:https://tools.ietf.org/html/rfc7235[RFC 7235]. @@ -88,4 +88,4 @@ authn.apply(request); request.send(); ---- -See also the link:#http-client-proxy-authentication[proxy authentication section] for further information about how authentication works with HTTP proxies. +See also the link:#client-http-proxy-authentication[proxy authentication section] for further information about how authentication works with HTTP proxies. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-configuration.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-configuration.adoc new file mode 100644 index 00000000000..e9a6f0ab7b1 --- /dev/null +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-configuration.adoc @@ -0,0 +1,86 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +[[client-http-configuration]] +=== HttpClient Configuration + +`HttpClient` has a quite large number of configuration parameters. +Please refer to the `HttpClient` +link:{JDURL}/org/eclipse/jetty/client/HttpClient.html[javadocs] +for the complete list of configurable parameters. +The most common parameters are: + +* `HttpClient.idleTimeout`: same as `ClientConnector.idleTimeout` +described in link:#client-io-arch-network[this section]. +* `HttpClient.connectBlocking`: same as `ClientConnector.connectBlocking` +described in link:#client-io-arch-network[this section]. +* `HttpClient.connectTimeout`: same as `ClientConnector.connectTimeout` +described in link:#client-io-arch-network[this section]. +* `HttpClient.maxConnectionsPerDestination`: the max number of TCP +connections that are opened for a particular destination (defaults to 64). +* `HttpClient.maxRequestsQueuedPerDestination`: the max number of requests +queued (defaults to 1024). + +[[client-http-configuration-tls]] +==== HttpClient TLS Configuration + +`HttpClient` supports HTTPS requests out-of-the-box like a browser does. + +The support for HTTPS request is provided by a `SslContextFactory.Client`, +typically configured in the `ClientConnector`. +If not explicitly configured, the `ClientConnector` will allocate a default +one when started. + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=tlsExplicit] +---- + +The default `SslContextFactory.Client` verifies the certificate sent by the +server by verifying the certificate chain. +This means that requests to public websites that have a valid certificates +(such as ``https://google.com``) will work out-of-the-box. + +However, requests made to sites (typically ``localhost``) that have invalid +(for example, expired or with a wrong host) or self-signed certificates will +fail (like they will in a browser). + +Certificate validation is performed at two levels: at the TLS implementation +level (in the JDK) and - optionally - at the application level. + +By default, certificate validation at the TLS level is enabled, while +certificate validation at the application level is disabled. + +You can configure the `SslContextFactory.Client` to skip certificate validation +at the TLS level: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=tlsNoValidation] +---- + +You can enable certificate validation at the application level: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=tlsAppValidation] +---- + +Please refer to the `SslContextFactory.Client` +link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.Client.html[javadocs] +for the complete list of configurable parameters. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-cookie.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-cookie.adoc similarity index 99% rename from jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-cookie.adoc rename to jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-cookie.adoc index 8d6dae298a5..62770598726 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-cookie.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-cookie.adoc @@ -16,7 +16,7 @@ // ======================================================================== // -[[http-client-cookie]] +[[client-http-cookie]] === Cookies Support Jetty HTTP client supports cookies out of the box. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-intro.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-intro.adoc new file mode 100644 index 00000000000..34f8c78547f --- /dev/null +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-intro.adoc @@ -0,0 +1,175 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +[[client-http-intro]] +=== HttpClient Introduction + +The Jetty HTTP client module provides easy-to-use APIs and utility classes to perform HTTP (or HTTPS) requests. + +Jetty's HTTP client is non-blocking and asynchronous. +It offers an asynchronous API that never blocks for I/O, making it very efficient in thread utilization and well suited for high performance scenarios such as load testing or parallel computation. + +However, when all you need to do is to perform a `GET` request to a resource, Jetty's HTTP client offers also a synchronous API; a programming interface +where the thread that issued the request blocks until the request/response conversation is complete. + +Jetty's HTTP client supports link:#http-client-transport[different transports]: HTTP/1.1, FastCGI and HTTP/2. +This means that the semantic of a HTTP request (that is, " `GET` me the resource `/index.html` ") can be carried over the network in different formats. +The most common and default format is HTTP/1.1. +That said, Jetty's HTTP client can carry the same request using the FastCGI format or the HTTP/2 format. + +The FastCGI transport is heavily used in Jetty's link:#fastcgi[FastCGI support] that allows Jetty to work as a reverse proxy to PHP (exactly like Apache or Nginx do) and therefore be able to serve - for example - WordPress websites. + +The HTTP/2 transport allows Jetty's HTTP client to perform requests using HTTP/2 to HTTP/2 enabled web sites, see also Jetty's link:#http2[HTTP/2 support]. + +Out of the box features that you get with the Jetty HTTP client include: + +* Redirect support - redirect codes such as 302 or 303 are automatically followed. +* Cookies support - cookies sent by servers are stored and sent back to servers in matching requests. +* Authentication support - HTTP "Basic" and "Digest" authentications are supported, others are pluggable. +* Forward proxy support - HTTP proxying and SOCKS4 proxying. + +[[client-http-start]] +==== Starting HttpClient + +The main class is named `org.eclipse.jetty.client.HttpClient`. + +You can think of a `HttpClient` instance as a browser instance. +Like a browser it can make requests to different domains, it manages redirects, cookies and authentication, you can configure it with a proxy, and +it provides you with the responses to the requests you make. + +In order to use `HttpClient`, you must instantiate it, configure it, and then start it: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=start] +---- + +You may create multiple instances of `HttpClient`, but typically one instance is enough for an application. +There are several reasons for having multiple `HttpClient` instances including, but not limited to: + +* You want to specify different configuration parameters (for example, one instance is configured with a forward proxy while another is not). +* You want the two instances to behave like two different browsers and hence have different cookies, different authentication credentials, etc. +* You want to use link:#http-client-transport[different transports]. + +Like browsers, HTTPS requests are supported out-of-the-box, as long as the server +provides a valid certificate. +In case the server does not provide a valid certificate (or in case it is self-signed) +you want to customize ``HttpClient``'s TLS configuration as described in +link:#client-http-configuration-tls[this section]. + +[[client-http-stop]] +==== Stopping HttpClient + +It is recommended that when your application stops, you also stop the `HttpClient` instance (or instances) that you are using. + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=stop] +---- + +Stopping `HttpClient` makes sure that the memory it holds (for example, authentication credentials, cookies, etc.) is released, and that the thread pool and scheduler are properly stopped allowing all threads used by `HttpClient` to exit. + +[[client-http-arch]] +==== HttpClient Architecture + +A `HttpClient` instance can be thought as a browser instance, and it manages the +following components: + +* a `CookieStore` (see link:#client-http-cookie[this section]). +* a `AuthenticationStore` (see link:#client-http-authentication[this section]). +* a `ProxyConfiguration` (see link:#client-http-proxy[this section]). +* a set of _destinations_. + +A _destination_ is the client-side component that represent an _origin_ on +a server, and manages a queue of requests for that origin, and a pool of +connections to that origin. + +An _origin_ may be simply thought as the tuple `(scheme, host, port)` and it +is where the client connects to in order to communicate with the server. +However, this is not enough. + +If you use `HttpClient` to write a proxy you may have different clients that +want to contact the same server. +In this case, you may not want to use the same proxy-to-server connection to +proxy requests for both clients, for example for authentication reasons: the +server may associate the connection with authentication credentials and you +do not want to use the same connection for two different users that have +different credentials. +Instead, you want to use different connections for different clients and +this can be achieved by "tagging" a destination with a tag object that +represents the remote client (for example, it could be the remote client IP +address). + +Two origins with the same `(scheme, host, port)` but different `tag` +create two different destinations and therefore two different connection pools. +However, also this is not enough. + +It is possible that a server speaks different protocols on the same `port`. +A connection may start by speaking one protocol, for example HTTP/1.1, but +then be upgraded to speak a different protocol, for example HTTP/2. +After a connection has been upgraded to a second protocol, it cannot speak +the first protocol anymore, so it can only be used to communicate using +the second protocol. + +Two origins with the same `(scheme, host, port)` but different +`protocol` create two different destinations and therefore two different +connection pools. + +Therefore an origin is identified by the tuple +`(scheme, host, port, tag, protocol)`. + +[[client-http-request-processing]] +==== HttpClient Request Processing + +When a request is sent, an origin is computed from the request; `HttpClient` +uses that origin to find (or create if it does not exist) the correspondent +destination. +The request is then queued onto the destination, and this causes the +destination to ask its connection pool for a free connection. +If a connection is available, it is returned, otherwise a new connection is +created. +Once the destination has obtained the connection, it dequeues the request +and sends it over the connection. + +The first request to a destination triggers the opening of the first +connection. +A second request with the same origin sent _after_ the first will reuse the +same connection. +A second request with the same origin sent _concurrently_ with the first +request will cause the opening of a second connection. +The configuration parameter `HttpClient.maxConnectionsPerDestination` +(see also the link:#client-http-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. + +Each connection can handle a limited number of requests. +For HTTP/1.1, this number is always `1`: there can only be one outstanding +request for each connection. +For HTTP/2 this number is determined by the server `max_concurrent_stream` +setting (typically around `100`, i.e. there can be up to `100` outstanding +requests for every connection). + +When a destination has maxed out its number of connections, and all +connections have maxed out their number of outstanding requests, more requests +sent to that destination will be queued. +When the request queue is full, the request will be failed. +The configuration parameter `HttpClient.maxRequestsQueuedPerDestination` +(see also the link:#client-http-configuration[configuration section]) controls +the max number of requests that can be queued for a destination. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-proxy.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-proxy.adoc similarity index 96% rename from jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-proxy.adoc rename to jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-proxy.adoc index 1d5acd92475..13f53584e93 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-proxy.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-proxy.adoc @@ -16,7 +16,7 @@ // ======================================================================== // -[[http-client-proxy]] +[[client-http-proxy]] === Proxy Support Jetty's HTTP client can be configured to use proxies to connect to destinations. @@ -44,10 +44,10 @@ You specify the proxy host and port, and optionally also the addresses that you Configured in this way, `HttpClient` makes requests to the HTTP proxy (for plain-text HTTP requests) or establishes a tunnel via `HTTP CONNECT` (for encrypted HTTPS requests). -[[http-client-proxy-authentication]] +[[client-http-proxy-authentication]] ==== Proxy Authentication Support -Jetty's HTTP client support proxy authentication in the same way it supports link:#http-client-authentication[server authentication]. +Jetty's HTTP client support proxy authentication in the same way it supports link:#client-http-authentication[server authentication]. In the example below, the proxy requires Basic authentication, but the server requires Digest authentication, and therefore: @@ -100,4 +100,4 @@ Application HttpClient Proxy Server The application does not receive events related to the responses with code 407 and 401 since they are handled internally by `HttpClient`. -Similarly to the link:#http-client-authentication[authentication section], the proxy authentication result and the server authentication result can be preempted to avoid, respectively, the 407 and 401 roundtrips. +Similarly to the link:#client-http-authentication[authentication section], the proxy authentication result and the server authentication result can be preempted to avoid, respectively, the 407 and 401 roundtrips. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-transport.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-transport.adoc similarity index 100% rename from jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-transport.adoc rename to jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-transport.adoc diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http.adoc index c1f403bd05e..53b5833b1cf 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http.adoc @@ -18,3 +18,12 @@ [[client-http]] === HTTP Client + +include::client-http-intro.adoc[] +include::client-http-api.adoc[] +include::client-http-configuration.adoc[] +include::client-http-cookie.adoc[] +include::client-http-authentication.adoc[] +include::client-http-proxy.adoc[] +include::client-http-transport.adoc[] + diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/io-arch.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/io-arch.adoc index e083aa1d6ee..178e04eaaa0 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/io-arch.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/io-arch.adoc @@ -261,3 +261,5 @@ Otherwise, the write is now `PENDING` and waiting for the callback to be notified of the completion at a later time. When the callback is notified of the `write()` completion, it checks whether the `write()` was `PENDING`, and if it was it resumes reading. + +NOTE: TODO: Introduce IteratingCallback? diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/chapter.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/chapter.adoc deleted file mode 100644 index fe028abc9b0..00000000000 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/chapter.adoc +++ /dev/null @@ -1,27 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -[[http-client]] -== HTTP Client - -include::http-client-intro.adoc[] -include::http-client-api.adoc[] -include::http-client-cookie.adoc[] -include::http-client-authentication.adoc[] -include::http-client-proxy.adoc[] -include::http-client-transport.adoc[] diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-api.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-api.adoc deleted file mode 100644 index 6d0cd736ae4..00000000000 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-api.adoc +++ /dev/null @@ -1,382 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -[[http-client-api]] -=== API Usage - -[[http-client-blocking]] -==== Blocking APIs - -The simple way to perform a HTTP request is the following: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.GET("http://domain.com/path?query"); ----- - -The method `HttpClient.GET(...)` performs a HTTP `GET` request to the given URI and returns a `ContentResponse` when the request/response conversation completes successfully. - -The `ContentResponse` object contains the HTTP response information: status code, headers and possibly content. -The content length is limited by default to 2 MiB; for larger content see xref:http-client-response-content[]. - -If you want to customize the request, for example by issuing a `HEAD` request instead of a `GET`, and simulating a browser user agent, you can do it in this way: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.newRequest("http://domain.com/path?query") - .method(HttpMethod.HEAD) - .agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0") - .send(); ----- - -This is a shorthand for: - -[source, java, subs="{sub-order}"] ----- -Request request = httpClient.newRequest("http://domain.com/path?query"); -request.method(HttpMethod.HEAD); -request.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0"); -ContentResponse response = request.send(); ----- - -You first create a request object using `httpClient.newRequest(...)`, and then you customize it using the fluent API style (that is, a chained invocation of methods on the request object). -When the request object is customized, you call `request.send()` that produces the `ContentResponse` when the request/response conversation is complete. - -Simple `POST` requests also have a shortcut method: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.POST("http://domain.com/entity/1") - .param("p", "value") - .send(); ----- - -The `POST` parameter values added via the `param()` method are automatically URL-encoded. - -Jetty's HTTP client automatically follows redirects, so it handles the typical web pattern http://en.wikipedia.org/wiki/Post/Redirect/Get[POST/Redirect/GET], and the response object contains the content of the response of the `GET` request. -Following redirects is a feature that you can enable/disable on a per-request basis or globally. - -File uploads also require one line, and make use of JDK 7′s `java.nio.file` classes: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.newRequest("http://domain.com/upload") - .method(HttpMethod.POST) - .file(Paths.get("file_to_upload.txt"), "text/plain") - .send(); ----- - -It is possible to impose a total timeout for the request/response conversation using the `Request.timeout(...)` method as follows: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.newRequest("http://domain.com/path?query") - .timeout(5, TimeUnit.SECONDS) - .send(); ----- - -In the example above, when the 5 seconds expire, the request is aborted and a `java.util.concurrent.TimeoutException` is thrown. - -[[http-client-async]] -==== Non-Blocking APIs - -So far we have shown how to use Jetty HTTP client in a blocking style - that is, the thread that issues the request blocks until the request/response conversation is complete. - -This section will look at Jetty's HTTP client non-blocking, asynchronous APIs that are perfectly suited for large content downloads, for parallel processing of requests/responses and in cases where performance and efficient thread and resource utilization is a key factor. - -The asynchronous APIs rely heavily on listeners that are invoked at various stages of request and response processing. -These listeners are implemented by applications and may perform any kind of logic. -The implementation invokes these listeners in the same thread that is used to process the request or response. -Therefore, if the application code in these listeners takes a long time to execute, the request or response processing is delayed until the listener returns. - -If you need to execute application code that takes long time inside a listener, you must spawn your own thread and remember to deep copy any data provided by the listener that you will need in your code, because when the listener returns the data it provides may be recycled/cleared/destroyed. - -Request and response processing are executed by two different threads and therefore may happen concurrently. -A typical example of this concurrent processing is an echo server, where a large upload may be concurrent with the large download echoed back. -As a side note, remember that responses may be processed and completed _before_ requests; a typical example is a large upload that triggers a quick response - for example an error - by the server: the response may arrive and be completed while the request content is still being uploaded. - -The application thread that calls `Request.send(Response.CompleteListener)` performs the processing of the request until either the request is fully processed or until it would block on I/O, then it returns (and therefore never blocks). -If it would block on I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns. -When such an event is fired, a thread taken from the `HttpClient` thread pool will resume the processing of the request. - -Response are processed from the I/O thread that fires the event that bytes are ready to be read. -Response processing continues until either the response is fully processed or until it would block for I/O. -If it would block for I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns. -When such an event is fired, a thread taken from the `HttpClient` thread pool will resume the processing of the response. - -When the request and the response are both fully processed, the thread that finished the last processing (usually the thread that processes the response, but may also be the thread that processes the request - if the request takes more time than the response to be processed) is used to de-queue the next request for the same destination and processes it. - -A simple asynchronous `GET` request that discards the response content can be written in this way: - -[source, java, subs="{sub-order}"] ----- -httpClient.newRequest("http://domain.com/path") - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) - { - // Your logic here - } - }); ----- - -Method `Request.send(Response.CompleteListener)` returns `void` and does not block; the `Response.CompleteListener` provided as a parameter is notified when the request/response conversation is complete, and the `Result` parameter allows you to access the response object. - -You can write the same code using JDK 8′s lambda expressions: - -[source, java, subs="{sub-order}"] ----- -httpClient.newRequest("http://domain.com/path") - .send(result -> { /* Your logic here */ }); ----- - -You can impose a total timeout for the request/response conversation in the same way used by the synchronous API: - -[source, java, subs="{sub-order}"] ----- -httpClient.newRequest("http://domain.com/path") - .timeout(3, TimeUnit.SECONDS) - .send(result -> { /* Your logic here */ }); ----- - -The example above will impose a total timeout of 3 seconds on the request/response conversation. - -The HTTP client APIs use listeners extensively to provide hooks for all possible request and response events, and with JDK 8′s lambda expressions they are even more fun to use: - -[source, java, subs="{sub-order}"] ----- -httpClient.newRequest("http://domain.com/path") - // Add request hooks - .onRequestQueued(request -> { ... }) - .onRequestBegin(request -> { ... }) - ... // More request hooks available - - // Add response hooks - .onResponseBegin(response -> { ... }) - .onResponseHeaders(response -> { ... }) - .onResponseContent((response, buffer) -> { ... }) - ... // More response hooks available - - .send(result -> { ... }); ----- - -This makes Jetty HTTP client suitable for HTTP load testing because, for example, you can accurately time every step of the request/response conversation (thus knowing where the request/response time is really spent). - -Have a look at the link:{JDURL}/org/eclipse/jetty/client/api/Request.Listener.html[`Request.Listener`] class to know about request events, and to the link:{JDURL}/org/eclipse/jetty/client/api/Response.Listener.html[`Response.Listener`] class to know about response events. - -[[http-client-content]] -==== Content Handling - -[[http-client-request-content]] -===== Request Content Handling - -Jetty's HTTP client provides a number of utility classes off the shelf to handle request content. - -You can provide request content as `String`, `byte[]`, `ByteBuffer`, `java.nio.file.Path`, `InputStream`, and provide your own implementation of `org.eclipse.jetty.client.api.Request.Content`. -Here’s an example that provides the request content using `java.nio.file.Paths`: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.newRequest("http://domain.com/upload") - .method(HttpMethod.POST) - .file(Paths.get("file_to_upload.txt"), "text/plain") - .send(); ----- - -This is equivalent to using the `PathRequestContent` utility class: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.newRequest("http://domain.com/upload") - .method(HttpMethod.POST) - .body(new PathRequestContent("text/plain", Paths.get("file_to_upload.txt"))) - .send(); ----- - -Alternatively, you can use `FileInputStream` via the `InputStreamRequestContent` utility class: - -[source, java, subs="{sub-order}"] ----- -ContentResponse response = httpClient.newRequest("http://domain.com/upload") - .method(HttpMethod.POST) - .body(new InputStreamRequestContent("text/plain", new FileInputStream("file_to_upload.txt"))) - .send(); ----- - -Since `InputStream` is blocking, then also the send of the request will block if the input stream blocks, even in case of usage of the asynchronous `HttpClient` APIs. - -If you have already read the content in memory, you can pass it as a `byte[]` using the `BytesRequestContent` utility class: - -[source, java, subs="{sub-order}"] ----- -byte[] bytes = ...; -ContentResponse response = httpClient.newRequest("http://domain.com/upload") - .method(HttpMethod.POST) - .body(new BytesRequestContent("text/plain", bytes)) - .send(); ----- - -If the request content is not immediately available, but your application will be notified of the content to send, you can use `AsyncRequestContent` in this way: - -[source, java, subs="{sub-order}"] ----- -AsyncRequestContent content = new AsyncRequestContent(); -httpClient.newRequest("http://domain.com/upload") - .method(HttpMethod.POST) - .body(content) - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) - { - // Your logic here - } - }); - -// Content not available yet here. - -... - -// An event happens, now content is available. -byte[] bytes = ...; -content.offer(ByteBuffer.wrap(bytes)); - -... - -// All content has arrived. -content.close(); ----- - -While the request content is awaited and consequently uploaded by the client application, the server may be able to respond (at least with the response headers) completely asynchronously. -In this case, `Response.Listener` callbacks will be invoked before the request is fully sent. -This allows fine-grained control of the request/response conversation: for example the server may reject contents that are too big, send a response to the client, which in turn may stop the content upload. - -Another way to provide request content is by using an `OutputStreamRequestContent`, -which allows applications to write request content when it is available to the `OutputStream` provided by `OutputStreamRequestContent`: - -[source, java, subs="{sub-order}"] ----- -OutputStreamRequestContent content = new OutputStreamRequestContent(); - -// Use try-with-resources to close the OutputStream when all content is written. -try (OutputStream output = content.getOutputStream()) -{ - client.newRequest("localhost", 8080) - .method(HttpMethod.POST) - .body(content) - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) - { - // Your logic here - } - }); - - ... - - // Write content. - byte[] bytes = ...; - output.write(bytes); -} -// End of try-with-resource, output.close() called automatically to signal end of content. ----- - -[[http-client-response-content]] -===== Response Content Handling - -Jetty HTTP client allows applications to handle response content in different ways. - -The first way is to buffer the response content in memory; this is done when using the blocking APIs (see xref:http-client-blocking[]) and the content is buffered within a `ContentResponse` up to 2 MiB. - -If you want to control the length of the response content (for example limiting to values smaller than the default of 2 MiB), then you can use a `org.eclipse.jetty.client.util.FutureResponseListener` in this way: - -[source, java, subs="{sub-order}"] ----- -Request request = httpClient.newRequest("http://domain.com/path"); - -// Limit response content buffer to 512 KiB -FutureResponseListener listener = new FutureResponseListener(request, 512 * 1024); - -request.send(listener); - -ContentResponse response = listener.get(5, TimeUnit.SECONDS); ----- - -If the response content length is exceeded, the response will be aborted, and an exception will be thrown by method `get()`. - -If you are using the asynchronous APIs (see xref:http-client-async[]), you can use the `BufferingResponseListener` utility class: - -[source, java, subs="{sub-order}"] ----- -httpClient.newRequest("http://domain.com/path") - // Buffer response content up to 8 MiB - .send(new BufferingResponseListener(8 * 1024 * 1024) - { - @Override - public void onComplete(Result result) - { - if (!result.isFailed()) - { - byte[] responseContent = getContent(); - // Your logic here - } - } - }); ----- - -The second way is the most efficient (because it avoids content copies) and allows you to specify a `Response.ContentListener`, or a subclass, to handle the content as soon as it arrives. -In the example below, `Response.Listener.Adapter` is a class that implements both `Response.ContentListener` and `Response.CompleteListener` and can be passed to `Request.send()`. -Jetty's HTTP client will invoke the `onContent()` method zero or more times (until there is content), and finally invoke the `onComplete()` method. - -[source, java, subs="{sub-order}"] ----- -httpClient .newRequest("http://domain.com/path") - .send(new Response.Listener.Adapter() - { - @Override - public void onContent(Response response, ByteBuffer buffer) - { - // Your logic here - } - }); ----- - -The third way allows you to wait for the response and then stream the content using the `InputStreamResponseListener` utility class: - -[source, java, subs="{sub-order}"] ----- - -InputStreamResponseListener listener = new InputStreamResponseListener(); -httpClient.newRequest("http://domain.com/path") - .send(listener); - -// Wait for the response headers to arrive -Response response = listener.get(5, TimeUnit.SECONDS); - -// Look at the response -if (response.getStatus() == HttpStatus.OK_200) -{ - // Use try-with-resources to close input stream. - try (InputStream responseContent = listener.getInputStream()) - { - // Your logic here - } -} ----- diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-intro.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-intro.adoc deleted file mode 100644 index 3a2f70d9c1b..00000000000 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/clients/http/http-client-intro.adoc +++ /dev/null @@ -1,105 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -[[http-client-intro]] -=== Introduction - -The Jetty HTTP client module provides easy-to-use APIs and utility classes to perform HTTP (or HTTPS) requests. - -Jetty's HTTP client is non-blocking and asynchronous. -It offers an asynchronous API that never blocks for I/O, making it very efficient in thread utilization and well suited for high performance scenarios such as load testing or parallel computation. - -However, when all you need to do is to perform a `GET` request to a resource, Jetty's HTTP client offers also a synchronous API; a programming interface -where the thread that issued the request blocks until the request/response conversation is complete. - -Jetty's HTTP client supports different link:#http-client-transport[transports]: HTTP/1.1, FastCGI and HTTP/2. -This means that the semantic of a HTTP request (that is, " `GET` me the resource `/index.html` ") can be carried over the network in different formats. -The most common and default format is HTTP/1.1. -That said, Jetty's HTTP client can carry the same request using the FastCGI format or the new HTTP/2 format. - -The FastCGI transport is heavily used in Jetty's link:#fastcgi[FastCGI support] that allows Jetty to work as a reverse proxy to PHP (exactly like Apache or Nginx do) and therefore be able to serve - for example - WordPress websites. - -The HTTP/2 transport allows Jetty's HTTP client to perform requests using HTTP/2 to HTTP/2 enabled web sites, see also Jetty's link:#http2[HTTP/2 support]. - -Out of the box features that you get with the Jetty HTTP client include: - -* Redirect support - redirect codes such as 302 or 303 are automatically followed. -* Cookies support - cookies sent by servers are stored and sent back to servers in matching requests. -* Authentication support - HTTP "Basic" and "Digest" authentications are supported, others are pluggable. -* Forward proxy support - HTTP proxying and SOCKS4 proxying. - -[[http-client-init]] -==== Starting HttpClient - -The main class is named `org.eclipse.jetty.client.HttpClient`. - -You can think of a `HttpClient` instance as a browser instance. -Like a browser it can make requests to different domains, it manages redirects, cookies and authentication, you can configure it with a proxy, and -it provides you with the responses to the requests you make. - -In order to use `HttpClient`, you must instantiate it, configure it, and then start it: - -[source, java, subs="{sub-order}"] ----- -// Instantiate HttpClient -HttpClient httpClient = new HttpClient(); - -// Configure HttpClient, for example: -httpClient.setFollowRedirects(false); - -// Start HttpClient -httpClient.start(); ----- - -You may create multiple instances of `HttpClient`, but typically one instance is enough for an application. -There are several reasons for having multiple `HttpClient` instances including, but not limited to: - -* You want to specify different configuration parameters (for example, one instance is configured with a forward proxy while another is not) -* You want the two instances to behave like two different browsers and hence have different cookies, different authentication credentials...etc. -* You want to use different transports - -When you create a `HttpClient` instance using the parameterless constructor, you will only be able to perform plain HTTP requests and you will not be able to perform HTTPS requests. - -In order to perform HTTPS requests, you should create first a link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.Client.html[`SslContextFactory.Client`], configure it, and pass it to the `HttpClient` constructor. -When created with a `SslContextFactory`, the `HttpClient` will be able to perform both HTTP and HTTPS requests to any domain. - -[source, java, subs="{sub-order}"] ----- -// Instantiate and configure the SslContextFactory -SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - -// Instantiate HttpClient with the SslContextFactory -HttpClient httpClient = new HttpClient(sslContextFactory); - -// Configure HttpClient, for example: -httpClient.setFollowRedirects(false); - -// Start HttpClient -httpClient.start(); ----- - -==== Stopping HttpClient - -It is recommended that when your application stops, you also stop the `HttpClient` instance (or instances) that you are using. - -[source, java, subs="{sub-order}"] ----- -httpClient.stop(); ----- - -Stopping `HttpClient` makes sure that the memory it holds (for example, authentication credentials, cookies, etc.) is released, and that the thread pool and scheduler are properly stopped allowing all threads used by `HttpClient` to exit. diff --git a/jetty-documentation/src/main/java/embedded/client/ClientConnectorDocs.java b/jetty-documentation/src/main/java/embedded/client/ClientConnectorDocs.java index 840bb63a0b2..5fcd35c789e 100644 --- a/jetty-documentation/src/main/java/embedded/client/ClientConnectorDocs.java +++ b/jetty-documentation/src/main/java/embedded/client/ClientConnectorDocs.java @@ -18,17 +18,27 @@ package embedded.client; +import java.io.ByteArrayOutputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.function.Consumer; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; @@ -108,9 +118,9 @@ public class ClientConnectorDocs public void connect() throws Exception { // tag::connect[] - class CustomHTTPConnection extends AbstractConnection + class CustomConnection extends AbstractConnection { - public CustomHTTPConnection(EndPoint endPoint, Executor executor) + public CustomConnection(EndPoint endPoint, Executor executor) { super(endPoint, executor); } @@ -119,6 +129,7 @@ public class ClientConnectorDocs public void onOpen() { super.onOpen(); + System.getLogger("connection").log(INFO, "Opened connection {0}", this); } @Override @@ -130,23 +141,289 @@ public class ClientConnectorDocs ClientConnector clientConnector = new ClientConnector(); clientConnector.start(); + String host = "serverHost"; + int port = 8080; + SocketAddress address = new InetSocketAddress(host, port); + + // The ClientConnectionFactory that creates CustomConnection instances. + ClientConnectionFactory connectionFactory = (endPoint, context) -> + { + System.getLogger("connection").log(INFO, "Creating connection for {0}", endPoint); + return new CustomConnection(endPoint, clientConnector.getExecutor()); + }; + + // The Promise to notify of connection creation success or failure. + CompletableFuture connectionPromise = new Promise.Completable<>(); + + // Populate the context with the mandatory keys to create and obtain connections. + Map context = new HashMap<>(); + context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); + clientConnector.connect(address, context); + + // Use the Connection when it's available. + + // Use it in a non-blocking way via CompletableFuture APIs. + connectionPromise.whenComplete((connection, failure) -> + { + System.getLogger("connection").log(INFO, "Created connection for {0}", connection); + }); + + // Alternatively, you can block waiting for the connection (or a failure). + // CustomConnection connection = connectionPromise.get(); + // end::connect[] + } + + public void telnet() throws Exception + { + // tag::telnet[] + class TelnetConnection extends AbstractConnection + { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private Consumer consumer; + + public TelnetConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + ByteBuffer buffer = BufferUtil.allocate(1024); + while (true) + { + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + while (buffer.hasRemaining()) + { + // Search for newline. + byte read = buffer.get(); + if (read == '\n') + { + // Notify the consumer of the line. + consumer.accept(bytes.toString(StandardCharsets.UTF_8)); + bytes.reset(); + } + else + { + bytes.write(read); + } + } + } + else if (filled == 0) + { + // No more bytes to fill, declare + // again interest for fill events. + fillInterested(); + return; + } + else + { + // The other peer closed the + // connection, close it back. + getEndPoint().close(); + return; + } + } + } + catch (Exception x) + { + getEndPoint().close(x); + } + } + + public void onLine(Consumer consumer) + { + this.consumer = consumer; + } + + public void writeLine(String line, Callback callback) + { + line = line + "\r\n"; + getEndPoint().write(callback, ByteBuffer.wrap(line.getBytes(StandardCharsets.UTF_8))); + } + } + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.start(); + String host = "wikipedia.org"; int port = 80; SocketAddress address = new InetSocketAddress(host, port); ClientConnectionFactory connectionFactory = (endPoint, context) -> - { - System.getLogger("connection").log(INFO, "Creating connection for {0}", endPoint); - return new CustomHTTPConnection(endPoint, clientConnector.getExecutor()); - }; + new TelnetConnection(endPoint, clientConnector.getExecutor()); + + CompletableFuture connectionPromise = new Promise.Completable<>(); + Map context = new HashMap<>(); context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); clientConnector.connect(address, context); - // end::connect[] + + connectionPromise.whenComplete((connection, failure) -> + { + if (failure == null) + { + // Register a listener that receives string lines. + connection.onLine(line -> System.getLogger("app").log(INFO, "line: {0}", line)); + + // Write a line. + connection.writeLine("" + + "GET / HTTP/1.0\r\n" + + "", Callback.NOOP); + } + else + { + failure.printStackTrace(); + } + }); + // end::telnet[] + } + + public void tlsTelnet() throws Exception + { + // tag::tlsTelnet[] + class TelnetConnection extends AbstractConnection + { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private Consumer consumer; + + public TelnetConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + ByteBuffer buffer = BufferUtil.allocate(1024); + while (true) + { + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + while (buffer.hasRemaining()) + { + // Search for newline. + byte read = buffer.get(); + if (read == '\n') + { + // Notify the consumer of the line. + consumer.accept(bytes.toString(StandardCharsets.UTF_8)); + bytes.reset(); + } + else + { + bytes.write(read); + } + } + } + else if (filled == 0) + { + // No more bytes to fill, declare + // again interest for fill events. + fillInterested(); + return; + } + else + { + // The other peer closed the + // connection, close it back. + getEndPoint().close(); + return; + } + } + } + catch (Exception x) + { + getEndPoint().close(x); + } + } + + public void onLine(Consumer consumer) + { + this.consumer = consumer; + } + + public void writeLine(String line, Callback callback) + { + line = line + "\r\n"; + getEndPoint().write(callback, ByteBuffer.wrap(line.getBytes(StandardCharsets.UTF_8))); + } + } + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.start(); + + // Use port 443 to contact the server using encrypted HTTP. + String host = "wikipedia.org"; + int port = 443; + SocketAddress address = new InetSocketAddress(host, port); + + ClientConnectionFactory connectionFactory = (endPoint, context) -> + new TelnetConnection(endPoint, clientConnector.getExecutor()); + + // Wrap the "telnet" ClientConnectionFactory with the SslClientConnectionFactory. + connectionFactory = new SslClientConnectionFactory(clientConnector.getSslContextFactory(), + clientConnector.getByteBufferPool(), clientConnector.getExecutor(), connectionFactory); + + // We will obtain a SslConnection now. + CompletableFuture connectionPromise = new Promise.Completable<>(); + + Map context = new HashMap<>(); + context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); + clientConnector.connect(address, context); + + connectionPromise.whenComplete((sslConnection, failure) -> + { + if (failure == null) + { + // Unwrap the SslConnection to access the "line" APIs in TelnetConnection. + TelnetConnection connection = (TelnetConnection)sslConnection.getDecryptedEndPoint().getConnection(); + // Register a listener that receives string lines. + connection.onLine(line -> System.getLogger("app").log(INFO, "line: {0}", line)); + + // Write a line. + connection.writeLine("" + + "GET / HTTP/1.0\r\n" + + "", Callback.NOOP); + } + else + { + failure.printStackTrace(); + } + }); + // end::tlsTelnet[] } public static void main(String[] args) throws Exception { - new ClientConnectorDocs().connect(); + new ClientConnectorDocs().tlsTelnet(); } } diff --git a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java new file mode 100644 index 00000000000..8549f712e64 --- /dev/null +++ b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java @@ -0,0 +1,477 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package embedded.client.http; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; +import java.util.function.LongConsumer; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.util.AsyncRequestContent; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.client.util.BytesRequestContent; +import org.eclipse.jetty.client.util.FutureResponseListener; +import org.eclipse.jetty.client.util.InputStreamRequestContent; +import org.eclipse.jetty.client.util.InputStreamResponseListener; +import org.eclipse.jetty.client.util.OutputStreamRequestContent; +import org.eclipse.jetty.client.util.PathRequestContent; +import org.eclipse.jetty.client.util.StringRequestContent; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class HTTPClientDocs +{ + public void start() throws Exception + { + // tag::start[] + // Instantiate HttpClient. + HttpClient httpClient = new HttpClient(); + + // Configure HttpClient, for example: + httpClient.setFollowRedirects(false); + + // Start HttpClient. + httpClient.start(); + // end::start[] + } + + public void stop() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + // tag::stop[] + // Stop HttpClient. + httpClient.stop(); + // end::stop[] + } + + public void tlsExplicit() throws Exception + { + // tag::tlsExplicit[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + httpClient.start(); + // end::tlsExplicit[] + } + + public void tlsNoValidation() + { + // tag::tlsNoValidation[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + // Disable certificate validation at the TLS level. + sslContextFactory.setEndpointIdentificationAlgorithm(null); + // end::tlsNoValidation[] + } + + public void tlsAppValidation() + { + // tag::tlsAppValidation[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + // Only allow subdomains of domain.com. + sslContextFactory.setHostnameVerifier((hostName, session) -> hostName.endsWith(".domain.com")); + // end::tlsAppValidation[] + } + + public void simpleBlockingGet() throws Exception + { + // tag::simpleBlockingGet[] + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // Perform a simple GET and wait for the response. + ContentResponse response = httpClient.GET("http://domain.com/path?query"); + // end::simpleBlockingGet[] + } + + public void headFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::headFluent[] + ContentResponse response = httpClient.newRequest("http://domain.com/path?query") + .method(HttpMethod.HEAD) + .agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0") + .send(); + // end::headFluent[] + } + + public void headNonFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::headNonFluent[] + Request request = httpClient.newRequest("http://domain.com/path?query"); + request.method(HttpMethod.HEAD); + request.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0"); + ContentResponse response = request.send(); + // end::headNonFluent[] + } + + public void postFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::postFluent[] + ContentResponse response = httpClient.POST("http://domain.com/entity/1") + .param("p", "value") + .send(); + // end::postFluent[] + } + + public void fileFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::fileFluent[] + ContentResponse response = httpClient.POST("http://domain.com/upload") + .file(Paths.get("file_to_upload.txt"), "text/plain") + .send(); + // end::fileFluent[] + } + + public void totalTimeout() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::totalTimeout[] + ContentResponse response = httpClient.newRequest("http://domain.com/path?query") + .timeout(5, TimeUnit.SECONDS) + .send(); + // end::totalTimeout[] + } + + public void simpleNonBlocking() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::simpleNonBlocking[] + httpClient.newRequest("http://domain.com/path") + .send(result -> + { + // Your logic here + }); + // end::simpleNonBlocking[] + } + + public void nonBlockingTotalTimeout() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::nonBlockingTotalTimeout[] + httpClient.newRequest("http://domain.com/path") + .timeout(3, TimeUnit.SECONDS) + .send(result -> + { + /* Your logic here */ + }); + // end::nonBlockingTotalTimeout[] + } + + // @checkstyle-disable-check : LeftCurly + public void listeners() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::listeners[] + httpClient.newRequest("http://domain.com/path") + // Add request hooks. + .onRequestQueued(request -> { /* ... */ }) + .onRequestBegin(request -> { /* ... */ }) + .onRequestHeaders(request -> { /* ... */ }) + .onRequestCommit(request -> { /* ... */ }) + .onRequestContent((request, content) -> { /* ... */ }) + .onRequestFailure((request, failure) -> { /* ... */ }) + .onRequestSuccess(request -> { /* ... */ }) + // Add response hooks. + .onResponseBegin(response -> { /* ... */ }) + .onResponseHeader((response, field) -> true) + .onResponseHeaders(response -> { /* ... */ }) + .onResponseContentAsync((response, buffer, callback) -> callback.succeeded()) + .onResponseFailure((response, failure) -> { /* ... */ }) + .onResponseSuccess(response -> { /* ... */ }) + // Result hook. + .send(result -> { /* ... */ }); + // end::listeners[] + } + + public void pathRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::pathRequestContent[] + ContentResponse response = httpClient.POST("http://domain.com/upload") + .body(new PathRequestContent("text/plain", Paths.get("file_to_upload.txt"))) + .send(); + // end::pathRequestContent[] + } + + public void inputStreamRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::inputStreamRequestContent[] + ContentResponse response = httpClient.POST("http://domain.com/upload") + .body(new InputStreamRequestContent("text/plain", new FileInputStream("file_to_upload.txt"))) + .send(); + // end::inputStreamRequestContent[] + } + + public void bytesStringRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + byte[] bytes = new byte[1024]; + String string = new String(bytes); + // tag::bytesStringRequestContent[] + ContentResponse bytesResponse = httpClient.POST("http://domain.com/upload") + .body(new BytesRequestContent("text/plain", bytes)) + .send(); + + ContentResponse stringResponse = httpClient.POST("http://domain.com/upload") + .body(new StringRequestContent("text/plain", string)) + .send(); + // end::bytesStringRequestContent[] + } + + public void asyncRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::asyncRequestContent[] + AsyncRequestContent content = new AsyncRequestContent(); + httpClient.POST("http://domain.com/upload") + .body(content) + .send(result -> + { + // Your logic here + }); + + // Content not available yet here. + + // An event happens in some other class, in some other thread. + class ContentPublisher + { + void publish(ByteBufferPool bufferPool, byte[] bytes, boolean lastContent) + { + // Wrap the bytes into a new ByteBuffer. + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + // Offer the content, and release the ByteBuffer + // to the pool when the Callback is completed. + content.offer(buffer, Callback.from(() -> bufferPool.release(buffer))); + + // Close AsyncRequestContent when all the content is arrived. + if (lastContent) + content.close(); + } + } + // end::asyncRequestContent[] + } + + public void outputStreamRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::outputStreamRequestContent[] + OutputStreamRequestContent content = new OutputStreamRequestContent(); + + // Use try-with-resources to close the OutputStream when all content is written. + try (OutputStream output = content.getOutputStream()) + { + httpClient.POST("http://localhost:8080/") + .body(content) + .send(result -> + { + // Your logic here + }); + + // Content not available yet here. + + // Content is now available. + byte[] bytes = new byte[]{'h', 'e', 'l', 'l', 'o'}; + output.write(bytes); + } + // End of try-with-resource, output.close() called automatically to signal end of content. + // end::outputStreamRequestContent[] + } + + public void futureResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::futureResponseListener[] + Request request = httpClient.newRequest("http://domain.com/path"); + + // Limit response content buffer to 512 KiB. + FutureResponseListener listener = new FutureResponseListener(request, 512 * 1024); + + request.send(listener); + + // Wait at most 5 seconds for request+response to complete. + ContentResponse response = listener.get(5, TimeUnit.SECONDS); + // end::futureResponseListener[] + } + + public void bufferingResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::bufferingResponseListener[] + httpClient.newRequest("http://domain.com/path") + // Buffer response content up to 8 MiB + .send(new BufferingResponseListener(8 * 1024 * 1024) + { + @Override + public void onComplete(Result result) + { + if (!result.isFailed()) + { + byte[] responseContent = getContent(); + // Your logic here + } + } + }); + // end::bufferingResponseListener[] + } + + public void inputStreamResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::inputStreamResponseListener[] + InputStreamResponseListener listener = new InputStreamResponseListener(); + httpClient.newRequest("http://domain.com/path") + .send(listener); + + // Wait for the response headers to arrive. + Response response = listener.get(5, TimeUnit.SECONDS); + + // Look at the response before streaming the content. + if (response.getStatus() == HttpStatus.OK_200) + { + // Use try-with-resources to close input stream. + try (InputStream responseContent = listener.getInputStream()) + { + // Your logic here + } + } + else + { + response.abort(new IOException("Unexpected HTTP response")); + } + // end::inputStreamResponseListener[] + } + + public void demandedContentListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + String host1 = "localhost"; + String host2 = "localhost"; + int port1 = 8080; + int port2 = 8080; + // tag::demandedContentListener[] + // Prepare a request to server1, the source. + Request request1 = httpClient.newRequest(host1, port1) + .path("/source"); + + // Prepare a request to server2, the sink. + AsyncRequestContent content2 = new AsyncRequestContent(); + Request request2 = httpClient.newRequest(host2, port2) + .path("/sink") + .body(content2); + + request1.onResponseContentDemanded(new Response.DemandedContentListener() + { + @Override + public void onBeforeContent(Response response, LongConsumer demand) + { + request2.onRequestCommit(request -> + { + // Only when the request to server2 has been sent, + // then demand response content from server1. + demand.accept(1); + }); + + // Send the request to server2. + request2.send(result -> System.getLogger("forwarder").log(INFO, "Forwarding to server2 complete")); + } + + @Override + public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback) + { + // When response content is received from server1, forward it to server2. + content2.offer(content, Callback.from(() -> + { + // When the request content to server2 is sent, + // succeed the callback to recycle the buffer. + callback.succeeded(); + // Then demand more response content from server1. + demand.accept(1); + }, callback::failed)); + } + }); + + // When the response content from server1 is complete, + // complete also the request content to server2. + request1.onResponseSuccess(response -> content2.close()); + + // Send the request to server1. + request1.send(result -> System.getLogger("forwarder").log(INFO, "Sourcing from server1 complete")); + // end::demandedContentListener[] + } +} diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index abe77a5673e..5d215abf78a 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -281,15 +282,7 @@ public class ClientConnector extends ContainerLifeCycle protected void safeClose(Closeable closeable) { - try - { - if (closeable != null) - closeable.close(); - } - catch (Throwable x) - { - LOG.trace("IGNORED", x); - } + IO.close(closeable); } protected void configure(SocketChannel channel) throws IOException @@ -330,6 +323,18 @@ public class ClientConnector extends ContainerLifeCycle return factory.newConnection(endPoint, context); } + @Override + public void connectionOpened(Connection connection, Object context) + { + super.connectionOpened(connection, context); + @SuppressWarnings("unchecked") + Map contextMap = (Map)context; + @SuppressWarnings("unchecked") + Promise promise = (Promise)contextMap.get(CONNECTION_PROMISE_CONTEXT_KEY); + if (promise != null) + promise.succeeded(connection); + } + @Override protected void connectionFailed(SelectableChannel channel, Throwable failure, Object attachment) { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java index afa3410f882..fbdd71ad77b 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java @@ -268,12 +268,13 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException { EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey); - Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment()); + Object context = selectionKey.attachment(); + Connection connection = _selectorManager.newConnection(channel, endPoint, context); endPoint.setConnection(connection); selectionKey.attach(endPoint); endPoint.onOpen(); endPointOpened(endPoint); - _selectorManager.connectionOpened(connection); + _selectorManager.connectionOpened(connection, context); if (LOG.isDebugEnabled()) LOG.debug("Created {}", endPoint); } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java index fb427e68291..1bcd0080ed8 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java @@ -302,8 +302,10 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump *

Callback method invoked when a connection is opened.

* * @param connection the connection just opened + * @param context the attachment associated with the creation of the connection + * @see #newConnection(SelectableChannel, EndPoint, Object) */ - public void connectionOpened(Connection connection) + public void connectionOpened(Connection connection, Object context) { try { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index d6bc2c74765..d19f93f98d8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -2368,7 +2368,7 @@ public class Request implements HttpServletRequest if (newQueryParams == null || newQueryParams.size() == 0) mergedQueryParams = oldQueryParams == null ? NO_PARAMS : oldQueryParams; else if (oldQueryParams == null || oldQueryParams.size() == 0) - mergedQueryParams = newQueryParams == null ? NO_PARAMS : newQueryParams; + mergedQueryParams = newQueryParams; else { // Parameters values are accumulated. @@ -2380,38 +2380,7 @@ public class Request implements HttpServletRequest resetParameters(); if (updateQueryString) - { - if (newQuery == null) - setQueryString(oldQuery); - else if (oldQuery == null) - setQueryString(newQuery); - else if (oldQueryParams.keySet().stream().anyMatch(newQueryParams.keySet()::contains)) - { - // Build the new merged query string, parameters in the - // new query string hide parameters in the old query string. - StringBuilder mergedQuery = new StringBuilder(); - if (newQuery != null) - mergedQuery.append(newQuery); - for (Map.Entry> entry : mergedQueryParams.entrySet()) - { - if (newQueryParams != null && newQueryParams.containsKey(entry.getKey())) - continue; - for (String value : entry.getValue()) - { - if (mergedQuery.length() > 0) - mergedQuery.append("&"); - URIUtil.encodePath(mergedQuery, entry.getKey()); - mergedQuery.append('='); - URIUtil.encodePath(mergedQuery, value); - } - } - setQueryString(mergedQuery.toString()); - } - else - { - setQueryString(newQuery + '&' + oldQuery); - } - } + setQueryString(newQuery == null ? oldQuery : newQuery); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index aae74e8e529..be4bc60cabc 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -1017,11 +1017,9 @@ public class Response implements HttpServletResponse _contentType = contentType; _mimeType = MimeTypes.CACHE.get(contentType); - String charset; - if (_mimeType != null && _mimeType.getCharset() != null && !_mimeType.isCharsetAssumed()) + String charset = MimeTypes.getCharsetFromContentType(contentType); + if (charset == null && _mimeType != null && _mimeType.isCharsetAssumed()) charset = _mimeType.getCharsetString(); - else - charset = MimeTypes.getCharsetFromContentType(contentType); if (charset == null) { @@ -1030,11 +1028,10 @@ public class Response implements HttpServletResponse case NOT_SET: break; case INFERRED: - case SET_CONTENT_TYPE: if (isWriting()) { - _mimeType = null; _contentType = _contentType + ";charset=" + _characterEncoding; + _mimeType = MimeTypes.CACHE.get(_contentType); } else { @@ -1042,11 +1039,12 @@ public class Response implements HttpServletResponse _characterEncoding = null; } break; + case SET_CONTENT_TYPE: case SET_LOCALE: case SET_CHARACTER_ENCODING: { _contentType = contentType + ";charset=" + _characterEncoding; - _mimeType = null; + _mimeType = MimeTypes.CACHE.get(_contentType); break; } default: @@ -1056,10 +1054,10 @@ public class Response implements HttpServletResponse else if (isWriting() && !charset.equalsIgnoreCase(_characterEncoding)) { // too late to change the character encoding; - _mimeType = null; _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType); - if (_characterEncoding != null) + if (_characterEncoding != null && (_mimeType == null || !_mimeType.isCharsetAssumed())) _contentType = _contentType + ";charset=" + _characterEncoding; + _mimeType = MimeTypes.CACHE.get(_contentType); } else { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java index 248eec4402a..32b6f1d2b57 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionDataStore.java @@ -709,21 +709,15 @@ public class JDBCSessionDataStore extends AbstractSessionDataStore statement.setLong(10, data.getExpiry()); statement.setLong(11, data.getMaxInactiveMs()); - if (!data.getAllAttributes().isEmpty()) + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) - { - SessionData.serializeAttributes(data, oos); - byte[] bytes = baos.toByteArray(); - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - statement.setBinaryStream(12, bais, bytes.length);//attribute map as blob - } - } - else - { - statement.setBinaryStream(12, EMPTY, 0); + SessionData.serializeAttributes(data, oos); + byte[] bytes = baos.toByteArray(); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + statement.setBinaryStream(12, bais, bytes.length);//attribute map as blob } + statement.executeUpdate(); if (LOG.isDebugEnabled()) LOG.debug("Inserted session " + data); @@ -746,23 +740,17 @@ public class JDBCSessionDataStore extends AbstractSessionDataStore statement.setLong(5, data.getExpiry()); statement.setLong(6, data.getMaxInactiveMs()); - if (!data.getAllAttributes().isEmpty()) + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) + SessionData.serializeAttributes(data, oos); + byte[] bytes = baos.toByteArray(); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) { - SessionData.serializeAttributes(data, oos); - byte[] bytes = baos.toByteArray(); - try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) - { - statement.setBinaryStream(7, bais, bytes.length);//attribute map as blob - } + statement.setBinaryStream(7, bais, bytes.length);//attribute map as blob } } - else - { - statement.setBinaryStream(7, EMPTY, 0); - } + statement.executeUpdate(); if (LOG.isDebugEnabled()) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index c8de27deeee..90ac8fd548a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -502,6 +502,17 @@ public class ResponseTest response.setCharacterEncoding("ISO-8859-1"); assertEquals("text/xml;charset=utf-8", response.getContentType()); } + + @Test + public void testContentEncodingViaContentTypeChange() throws Exception + { + Response response = getResponse(); + response.setContentType("text/html;charset=Shift_Jis"); + assertEquals("Shift_Jis", response.getCharacterEncoding()); + + response.setContentType("text/xml"); + assertEquals("Shift_Jis", response.getCharacterEncoding()); + } @Test public void testCharacterEncodingContentType() throws Exception @@ -625,7 +636,7 @@ public class ResponseTest response.setContentType("wrong/answer;charset=utf-8"); response.setContentType("foo/bar"); - assertEquals("foo/bar", response.getContentType()); + assertEquals("foo/bar;charset=utf-8", response.getContentType()); response.setContentType("wrong/answer;charset=utf-8"); response.getWriter(); response.setContentType("foo2/bar2;charset=utf-16"); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextDispatchWithQueryStrings.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextDispatchWithQueryStrings.java index 48f99451f0a..43c0e0ae024 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextDispatchWithQueryStrings.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextDispatchWithQueryStrings.java @@ -105,14 +105,14 @@ public class AsyncContextDispatchWithQueryStrings { AsyncContext async = request.startAsync(); async.dispatch("/secondDispatchNewValueForExistingQueryString?newQueryString=newValue"); - assertEquals("newQueryString=initialValue&initialParam=right", queryString); + assertEquals("newQueryString=initialValue", queryString); } else { response.setContentType("text/html"); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println("

woohhooooo

"); - assertEquals("newQueryString=newValue&initialParam=right", queryString); + assertEquals("newQueryString=newValue", queryString); } } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java index 85faed909ea..94ade753044 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java @@ -534,12 +534,12 @@ public class AsyncServletTest assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(__history, contains( "FWD REQUEST /ctx/fwd/info?start=200&dispatch=20", - "FORWARD /ctx/path1?forward=true&start=200&dispatch=20", + "FORWARD /ctx/path1?forward=true", "initial", "start", "dispatch", "FWD ASYNC /ctx/fwd/info?start=200&dispatch=20", - "FORWARD /ctx/path1?forward=true&start=200&dispatch=20", + "FORWARD /ctx/path1?forward=true", "!initial", "onComplete")); assertContains("DISPATCHED", response); @@ -552,7 +552,7 @@ public class AsyncServletTest assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(__history, contains( "FWD REQUEST /ctx/fwd/info?start=200&dispatch=20&path=/path2", - "FORWARD /ctx/path1?forward=true&start=200&dispatch=20&path=/path2", + "FORWARD /ctx/path1?forward=true", "initial", "start", "dispatch", @@ -569,11 +569,11 @@ public class AsyncServletTest assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(__history, contains( "FWD REQUEST /ctx/fwd/info?wrap=true&start=200&dispatch=20", - "FORWARD /ctx/path1?forward=true&wrap=true&start=200&dispatch=20", + "FORWARD /ctx/path1?forward=true", "initial", "start", "dispatch", - "ASYNC /ctx/path1?forward=true&wrap=true&start=200&dispatch=20", + "ASYNC /ctx/path1?forward=true", "wrapped REQ RSP", "!initial", "onComplete")); @@ -587,11 +587,11 @@ public class AsyncServletTest assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(__history, contains( "FWD REQUEST /ctx/fwd/info?wrap=true&start=200&dispatch=20&path=/path2", - "FORWARD /ctx/path1?forward=true&wrap=true&start=200&dispatch=20&path=/path2", + "FORWARD /ctx/path1?forward=true", "initial", "start", "dispatch", - "ASYNC /ctx/path2?forward=true&wrap=true&start=200&dispatch=20&path=/path2", + "ASYNC /ctx/path2?forward=true", "wrapped REQ RSP", "!initial", "onComplete")); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherForwardTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherForwardTest.java index f839562522f..5d420d71beb 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherForwardTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherForwardTest.java @@ -142,7 +142,6 @@ public class DispatcherForwardTest CountDownLatch latch = new CountDownLatch(1); final String query1 = "a=1%20one&b=2%20two"; final String query2 = "a=3%20three"; - final String query3 = "a=3%20three&b=2%20two"; servlet1 = new HttpServlet() { @Override @@ -163,7 +162,7 @@ public class DispatcherForwardTest @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - checkThat(req.getQueryString(), Matchers.equalTo(query3)); + checkThat(req.getQueryString(), Matchers.equalTo(query2)); checkThat(req.getParameter("a"), Matchers.equalTo("3 three")); checkThat(req.getParameter("b"), Matchers.equalTo("2 two")); } @@ -186,13 +185,12 @@ public class DispatcherForwardTest { // 1. request /one?a=1 // 1. forward /two?b=2 - // 2. assert query => a=1&b=2 + // 2. assert query => b=2 // 1. assert query => a=1 CountDownLatch latch = new CountDownLatch(1); final String query1 = "a=1%20one"; final String query2 = "b=2%20two"; - final String query3 = "b=2%20two&a=1%20one"; servlet1 = new HttpServlet() { @Override @@ -212,7 +210,7 @@ public class DispatcherForwardTest @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - checkThat(req.getQueryString(), Matchers.equalTo(query3)); + checkThat(req.getQueryString(), Matchers.equalTo(query2)); checkThat(req.getParameter("a"), Matchers.equalTo("1 one")); checkThat(req.getParameter("b"), Matchers.equalTo("2 two")); } @@ -348,13 +346,12 @@ public class DispatcherForwardTest { // 1. request /one?a=1 + content b=2 // 1. forward /two?c=3 - // 2. assert query => a=1&c=3 + params => a=1&b=2&c=3 + // 2. assert query => c=3 + params => a=1&b=2&c=3 // 1. assert query => a=1 + params => a=1&b=2 CountDownLatch latch = new CountDownLatch(1); final String query1 = "a=1%20one"; final String query2 = "c=3%20three"; - final String query3 = "c=3%20three&a=1%20one"; final String form = "b=2%20two"; servlet1 = new HttpServlet() { @@ -377,7 +374,7 @@ public class DispatcherForwardTest @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - checkThat(req.getQueryString(), Matchers.equalTo(query3)); + checkThat(req.getQueryString(), Matchers.equalTo(query2)); checkThat(req.getParameter("a"), Matchers.equalTo("1 one")); checkThat(req.getParameter("b"), Matchers.equalTo("2 two")); checkThat(req.getParameter("c"), Matchers.equalTo("3 three")); @@ -405,13 +402,12 @@ public class DispatcherForwardTest // 1. request /one?a=1 + content b=2 // 1. assert params => a=1&b=2 // 1. forward /two?c=3 - // 2. assert query => a=1&c=3 + params => a=1&b=2&c=3 + // 2. assert query => c=3 + params => a=1&b=2&c=3 // 1. assert query => a=1 + params => a=1&b=2 CountDownLatch latch = new CountDownLatch(1); final String query1 = "a=1%20one"; final String query2 = "c=3%20three"; - final String query3 = "c=3%20three&a=1%20one"; final String form = "b=2%20two"; servlet1 = new HttpServlet() { @@ -436,7 +432,7 @@ public class DispatcherForwardTest @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - checkThat(req.getQueryString(), Matchers.equalTo(query3)); + checkThat(req.getQueryString(), Matchers.equalTo(query2)); checkThat(req.getParameter("a"), Matchers.equalTo("1 one")); checkThat(req.getParameter("b"), Matchers.equalTo("2 two")); checkThat(req.getParameter("c"), Matchers.equalTo("3 three")); @@ -513,7 +509,6 @@ public class DispatcherForwardTest CountDownLatch latch = new CountDownLatch(1); final String query1 = "a=1%20one"; final String query2 = "b=2%20two"; - final String query3 = "b=2%20two&a=1%20one"; final String form = "c=3%20three"; servlet1 = new HttpServlet() { @@ -534,7 +529,7 @@ public class DispatcherForwardTest @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - checkThat(req.getQueryString(), Matchers.equalTo(query3)); + checkThat(req.getQueryString(), Matchers.equalTo(query2)); ServletInputStream input = req.getInputStream(); for (int i = 0; i < form.length(); ++i) { diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherTest.java index ca341f693bc..e3ed6c25b81 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DispatcherTest.java @@ -744,7 +744,7 @@ public class DispatcherTest assertEquals(null, request.getPathInfo()); assertEquals(null, request.getPathTranslated()); - assertEquals("do=end&do=the&test=1", request.getQueryString()); + assertEquals("do=end&do=the", request.getQueryString()); assertEquals("/context/AssertForwardServlet", request.getRequestURI()); assertEquals("/context", request.getContextPath()); assertEquals("/AssertForwardServlet", request.getServletPath()); @@ -789,8 +789,8 @@ public class DispatcherTest q2.decode(query.getString("else")); String russian = q2.encode(); assertThat(russian, is("%D0%B2%D1%8B%D0%B1%D1%80%D0%B0%D0%BD%D0%BE=%D0%A2%D0%B5%D0%BC%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D1%83%D1%80%D0%B0")); - assertThat(query.getString("test"), is("1")); - assertThat(query.containsKey("foreign"), is(true)); + assertThat(query.containsKey("test"), is(false)); + assertThat(query.containsKey("foreign"), is(false)); String[] vals = request.getParameterValues("foreign"); assertTrue(vals != null); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index 30f5b4b0520..317ea54b692 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -43,6 +43,8 @@ import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.HttpChannel; @@ -60,6 +62,7 @@ import org.slf4j.LoggerFactory; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -101,6 +104,7 @@ public class ErrorPageTest _context.addServlet(UnavailableServlet.class, "/unavailable/*"); _context.addServlet(DeleteServlet.class, "/delete/*"); _context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*"); + _context.addServlet(ErrorContentTypeCharsetWriterInitializedServlet.class, "/error-mime-charset-writer/*"); HandlerWrapper noopHandler = new HandlerWrapper() { @@ -140,6 +144,36 @@ public class ErrorPageTest _server.join(); } + @Test + public void testErrorOverridesMimeTypeAndCharset() throws Exception + { + StringBuilder rawRequest = new StringBuilder(); + rawRequest.append("GET /error-mime-charset-writer/ HTTP/1.1\r\n"); + rawRequest.append("Host: test\r\n"); + rawRequest.append("Connection: close\r\n"); + rawRequest.append("Accept: */*\r\n"); + rawRequest.append("Accept-Charset: *\r\n"); + rawRequest.append("\r\n"); + + String rawResponse = _connector.getResponse(rawRequest.toString()); + System.out.println(rawResponse); + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat(response.getStatus(), is(595)); + String actualContentType = response.get(HttpHeader.CONTENT_TYPE); + // should not expect to see charset line from servlet + assertThat(actualContentType, not(containsString("charset=US-ASCII"))); + String body = response.getContent(); + + assertThat(body, containsString("ERROR_PAGE: /595")); + assertThat(body, containsString("ERROR_MESSAGE: 595")); + assertThat(body, containsString("ERROR_CODE: 595")); + assertThat(body, containsString("ERROR_EXCEPTION: null")); + assertThat(body, containsString("ERROR_EXCEPTION_TYPE: null")); + assertThat(body, containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$ErrorContentTypeCharsetWriterInitializedServlet-")); + assertThat(body, containsString("ERROR_REQUEST_URI: /error-mime-charset-writer/")); + } + @Test public void testErrorOverridesStatus() throws Exception { @@ -612,6 +646,18 @@ public class ErrorPageTest } } + public static class ErrorContentTypeCharsetWriterInitializedServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.setContentType("text/html; charset=US-ASCII"); + PrintWriter writer = response.getWriter(); + writer.println("Intentionally using sendError(595)"); + response.sendError(595); + } + } + public static class ErrorAndStatusServlet extends HttpServlet implements Servlet { @Override diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ResponseHeadersTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ResponseHeadersTest.java index 1221c9aba66..e9f73c6b995 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ResponseHeadersTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ResponseHeadersTest.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.servlet; import java.io.IOException; +import java.io.PrintWriter; import java.net.URLDecoder; import java.nio.ByteBuffer; @@ -73,6 +74,65 @@ public class ResponseHeadersTest } } + public static class CharsetResetToJsonMimeTypeServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + // We set an initial desired behavior. + response.setContentType("text/html; charset=US-ASCII"); + PrintWriter writer = response.getWriter(); + + // We reset the response, as we don't want it to be HTML anymore. + response.reset(); + + // switch to json operation + // The use of application/json is always assumed to be UTF-8 + // and should never have a `charset=` entry on the `Content-Type` response header + response.setContentType("application/json"); + writer.println("{ \"what\": \"should this be?\" }"); + } + } + + public static class CharsetChangeToJsonMimeTypeServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + // We set an initial desired behavior. + response.setContentType("text/html; charset=US-ASCII"); + + // switch to json behavior. + // The use of application/json is always assumed to be UTF-8 + // and should never have a `charset=` entry on the `Content-Type` response header + response.setContentType("application/json"); + + PrintWriter writer = response.getWriter(); + writer.println("{ \"what\": \"should this be?\" }"); + } + } + + public static class CharsetChangeToJsonMimeTypeSetCharsetToNullServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + // We set an initial desired behavior. + + response.setContentType("text/html; charset=us-ascii"); + PrintWriter writer = response.getWriter(); + + // switch to json behavior. + // The use of application/json is always assumed to be UTF-8 + // and should never have a `charset=` entry on the `Content-Type` response header + response.setContentType("application/json"); + // attempt to indicate that there is truly no charset meant to be used in the response header + response.setCharacterEncoding(null); + + writer.println("{ \"what\": \"should this be?\" }"); + } + } + private static Server server; private static LocalConnector connector; @@ -89,6 +149,9 @@ public class ResponseHeadersTest context.addServlet(new ServletHolder(new SimulateUpgradeServlet()), "/ws/*"); context.addServlet(new ServletHolder(new MultilineResponseValueServlet()), "/multiline/*"); + context.addServlet(CharsetResetToJsonMimeTypeServlet.class, "/charset/json-reset/*"); + context.addServlet(CharsetChangeToJsonMimeTypeServlet.class, "/charset/json-change/*"); + context.addServlet(CharsetChangeToJsonMimeTypeSetCharsetToNullServlet.class, "/charset/json-change-null/*"); server.start(); } @@ -149,4 +212,64 @@ public class ResponseHeadersTest expected = expected.trim(); // trim whitespace at start/end assertThat("Response Header X-example", response.get("X-Example"), is(expected)); } + + @Test + public void testCharsetResetToJsonMimeType() throws Exception + { + HttpTester.Request request = new HttpTester.Request(); + request.setMethod("GET"); + request.setURI("/charset/json-reset/"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Connection", "close"); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = connector.getResponse(request.generate()); + // System.err.println(BufferUtil.toUTF8String(responseBuffer)); + HttpTester.Response response = HttpTester.parseResponse(responseBuffer); + + // Now test for properly formatted HTTP Response Headers. + assertThat("Response Code", response.getStatus(), is(200)); + // The Content-Type should not have a charset= portion + assertThat("Response Header Content-Type", response.get("Content-Type"), is("application/json")); + } + + @Test + public void testCharsetChangeToJsonMimeType() throws Exception + { + HttpTester.Request request = new HttpTester.Request(); + request.setMethod("GET"); + request.setURI("/charset/json-change/"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Connection", "close"); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = connector.getResponse(request.generate()); + // System.err.println(BufferUtil.toUTF8String(responseBuffer)); + HttpTester.Response response = HttpTester.parseResponse(responseBuffer); + + // Now test for properly formatted HTTP Response Headers. + assertThat("Response Code", response.getStatus(), is(200)); + // The Content-Type should not have a charset= portion + assertThat("Response Header Content-Type", response.get("Content-Type"), is("application/json")); + } + + @Test + public void testCharsetChangeToJsonMimeTypeSetCharsetToNull() throws Exception + { + HttpTester.Request request = new HttpTester.Request(); + request.setMethod("GET"); + request.setURI("/charset/json-change-null/"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Connection", "close"); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = connector.getResponse(request.generate()); + // System.err.println(BufferUtil.toUTF8String(responseBuffer)); + HttpTester.Response response = HttpTester.parseResponse(responseBuffer); + + // Now test for properly formatted HTTP Response Headers. + assertThat("Response Code", response.getStatus(), is(200)); + // The Content-Type should not have a charset= portion + assertThat("Response Header Content-Type", response.get("Content-Type"), is("application/json")); + } } diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java index 06f841627e3..2d215c2ec57 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java @@ -196,14 +196,14 @@ public class Module implements Comparable process(basehome); } - public static boolean isRequiredDependency(String depends) + public static boolean isConditionalDependency(String depends) { - return (depends != null) && (depends.charAt(0) != '?'); + return (depends != null) && (depends.charAt(0) == '?'); } public static String normalizeModuleName(String name) { - if (!isRequiredDependency(name)) + if (isConditionalDependency(name)) return name.substring(1); return name; } diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java index 64b9e5a9450..3e3c200beda 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java @@ -136,10 +136,8 @@ public class Modules implements Iterable { parent = Module.normalizeModuleName(parent); System.out.printf(label, parent); - if (!Module.isRequiredDependency(parent)) - { - System.out.print(" [not-required]"); - } + if (Module.isConditionalDependency(parent)) + System.out.print(" [conditional]"); label = ", %s"; } System.out.println(); @@ -420,7 +418,7 @@ public class Modules implements Iterable StartLog.debug("Enabled module [%s] depends on %s", module.getName(), module.getDepends()); for (String dependsOnRaw : module.getDepends()) { - boolean isRequired = Module.isRequiredDependency(dependsOnRaw); + boolean isConditional = Module.isConditionalDependency(dependsOnRaw); // Final to allow lambda's below to use name final String dependentModule = Module.normalizeModuleName(dependsOnRaw); @@ -436,7 +434,7 @@ public class Modules implements Iterable if (dependentModule.contains("/")) { Path file = _baseHome.getPath("modules/" + dependentModule + ".mod"); - if (isRequired || Files.exists(file)) + if (!isConditional || Files.exists(file)) { registerModule(file).expandDependencies(_args.getProperties()); providers = _provided.get(dependentModule); @@ -447,10 +445,10 @@ public class Modules implements Iterable continue; } } - // is this a non-required module - if (!isRequired) + // is this a conditional module + if (isConditional) { - StartLog.debug("Skipping non-required module [%s]: doesn't exist", dependentModule); + StartLog.debug("Skipping conditional module [%s]: it does not exist", dependentModule); continue; } // throw an exception (not a dynamic module and a required dependency) @@ -570,7 +568,7 @@ public class Modules implements Iterable { // Check dependencies m.getDepends().stream() - .filter(Module::isRequiredDependency) + .filter(depends -> !Module.isConditionalDependency(depends)) .forEach(d -> { Set providers = getAvailableProviders(d); diff --git a/jetty-start/src/test/java/org/eclipse/jetty/start/ModulesTest.java b/jetty-start/src/test/java/org/eclipse/jetty/start/ModulesTest.java index 756f7db6404..65627f1c166 100644 --- a/jetty-start/src/test/java/org/eclipse/jetty/start/ModulesTest.java +++ b/jetty-start/src/test/java/org/eclipse/jetty/start/ModulesTest.java @@ -234,6 +234,7 @@ public class ModulesTest // Collect active module list List active = modules.getEnabled(); + modules.checkEnabledModules(); // Assert names are correct, and in the right order List expectedNames = new ArrayList<>(); @@ -282,6 +283,7 @@ public class ModulesTest // Collect active module list List active = modules.getEnabled(); + modules.checkEnabledModules(); // Assert names are correct, and in the right order List expectedNames = new ArrayList<>(); @@ -331,6 +333,7 @@ public class ModulesTest // Collect active module list List active = modules.getEnabled(); + modules.checkEnabledModules(); // Assert names are correct, and in the right order List expectedNames = new ArrayList<>(); diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreTest.java index 5a4a66a4871..d41296f3a52 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JDBCSessionDataStoreTest.java @@ -49,19 +49,16 @@ public class JDBCSessionDataStoreTest extends AbstractSessionDataStoreTest public void persistSession(SessionData data) throws Exception { - JdbcTestHelper.insertSession(data.getId(), data.getContextPath(), data.getVhost(), data.getLastNode(), - data.getCreated(), data.getAccessed(), data.getLastAccessed(), - data.getMaxInactiveMs(), data.getExpiry(), data.getCookieSet(), - data.getLastSaved(), data.getAllAttributes()); + JdbcTestHelper.insertSession(data); } @Override public void persistUnreadableSession(SessionData data) throws Exception { - JdbcTestHelper.insertSession(data.getId(), data.getContextPath(), data.getVhost(), data.getLastNode(), + JdbcTestHelper.insertUnreadableSession(data.getId(), data.getContextPath(), data.getVhost(), data.getLastNode(), data.getCreated(), data.getAccessed(), data.getLastAccessed(), data.getMaxInactiveMs(), data.getExpiry(), data.getCookieSet(), - data.getLastSaved(), null); + data.getLastSaved()); } @Override diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestHelper.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestHelper.java index 58222190f94..2ff961416d1 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestHelper.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestHelper.java @@ -29,7 +29,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; @@ -131,6 +130,35 @@ public class JdbcTestHelper sessionTableSchema.prepareTables(); } + + public static void dumpRow(ResultSet row) throws SQLException + { + if (row != null) + { + String id = row.getString(ID_COL); + long created = row.getLong(CREATE_COL); + long accessed = row.getLong(ACCESS_COL); + long lastAccessed = row.getLong(LAST_ACCESS_COL); + long maxIdle = row.getLong(MAX_IDLE_COL); + long cookieSet = row.getLong(COOKIE_COL); + String node = row.getString(LAST_NODE_COL); + long expires = row.getLong(EXPIRY_COL); + long lastSaved = row.getLong(LAST_SAVE_COL); + String context = row.getString(CONTEXT_COL); + Blob blob = row.getBlob(MAP_COL); + + String dump = "id=" + id + + " ctxt=" + context + + " node=" + node + + " exp=" + expires + + " acc=" + accessed + + " lacc=" + lastAccessed + + " ck=" + cookieSet + + " lsv=" + lastSaved + + " blob length=" + blob.length(); + System.err.println(dump); + } + } public static boolean existsInSessionTable(String id, boolean verbose) throws Exception @@ -151,6 +179,7 @@ public class JdbcTestHelper while (result.next()) { results = true; + dumpRow(result); } return results; } @@ -233,41 +262,53 @@ public class JdbcTestHelper return true; } - - public static void insertSession(String id, String contextPath, String vhost) - throws Exception + + public static void insertSession(SessionData data) throws Exception { + Class.forName(DRIVER_CLASS); try (Connection con = DriverManager.getConnection(DEFAULT_CONNECTION_URL);) { PreparedStatement statement = con.prepareStatement("insert into " + TABLE + " (" + ID_COL + ", " + CONTEXT_COL + ", virtualHost, " + LAST_NODE_COL + ", " + ACCESS_COL + ", " + LAST_ACCESS_COL + ", " + CREATE_COL + ", " + COOKIE_COL + - ", " + LAST_SAVE_COL + ", " + EXPIRY_COL + " " + ") " + - " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + ", " + LAST_SAVE_COL + ", " + EXPIRY_COL + ", " + MAX_IDLE_COL + "," + MAP_COL + " ) " + + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - statement.setString(1, id); - statement.setString(2, contextPath); - statement.setString(3, vhost); - statement.setString(4, "0"); + statement.setString(1, data.getId()); + statement.setString(2, data.getContextPath()); + statement.setString(3, data.getVhost()); + statement.setString(4, data.getLastNode()); - statement.setLong(5, System.currentTimeMillis()); - statement.setLong(6, System.currentTimeMillis()); - statement.setLong(7, System.currentTimeMillis()); - statement.setLong(8, System.currentTimeMillis()); + statement.setLong(5, data.getAccessed()); + statement.setLong(6, data.getLastAccessed()); + statement.setLong(7, data.getCreated()); + statement.setLong(8, data.getCookieSet()); - statement.setLong(9, System.currentTimeMillis()); - statement.setLong(10, System.currentTimeMillis()); + statement.setLong(9, data.getLastSaved()); + statement.setLong(10, data.getExpiry()); + statement.setLong(11, data.getMaxInactiveMs()); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos);) + { + SessionData.serializeAttributes(data, oos); + byte[] bytes = baos.toByteArray(); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);) + { + statement.setBinaryStream(12, bais, bytes.length); + } + } statement.execute(); assertEquals(1, statement.getUpdateCount()); } } - public static void insertSession(String id, String contextPath, String vhost, + public static void insertUnreadableSession(String id, String contextPath, String vhost, String lastNode, long created, long accessed, long lastAccessed, long maxIdle, long expiry, - long cookieSet, long lastSaved, Map attributes) + long cookieSet, long lastSaved) throws Exception { Class.forName(DRIVER_CLASS); @@ -293,23 +334,7 @@ public class JdbcTestHelper statement.setLong(10, expiry); statement.setLong(11, maxIdle); - if (attributes != null) - { - SessionData tmp = new SessionData(id, contextPath, vhost, created, accessed, lastAccessed, maxIdle); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos);) - { - SessionData.serializeAttributes(tmp, oos); - byte[] bytes = baos.toByteArray(); - - try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);) - { - statement.setBinaryStream(12, bais, bytes.length); - } - } - } - else - statement.setBinaryStream(12, new ByteArrayInputStream("".getBytes()), 0); + statement.setBinaryStream(12, new ByteArrayInputStream("".getBytes()), 0); statement.execute(); assertEquals(1, statement.getUpdateCount()); diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionTableSchemaTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionTableSchemaTest.java index 3c644ad2abb..40ef5a5f0fe 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionTableSchemaTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionTableSchemaTest.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.server.session; import java.io.ByteArrayInputStream; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -67,6 +68,46 @@ public class SessionTableSchemaTest JdbcTestHelper.shutdown(null); } + /** + * This inserts a session into the db that does not set the session attributes MAP column. As such + * this results in a row that is unreadable by the JDBCSessionDataStore, but is readable by using + * only jdbc api, which is what this test does. + * + * @param id id of session + * @param contextPath the context path of the session + * @param vhost the virtual host of the session + * @throws Exception + */ + public static void insertSessionWithoutAttributes(String id, String contextPath, String vhost) + throws Exception + { + Class.forName(JdbcTestHelper.DRIVER_CLASS); + try (Connection con = DriverManager.getConnection(JdbcTestHelper.DEFAULT_CONNECTION_URL);) + { + PreparedStatement statement = con.prepareStatement("insert into " + JdbcTestHelper.TABLE + + " (" + JdbcTestHelper.ID_COL + ", " + JdbcTestHelper.CONTEXT_COL + ", virtualHost, " + JdbcTestHelper.LAST_NODE_COL + + ", " + JdbcTestHelper.ACCESS_COL + ", " + JdbcTestHelper.LAST_ACCESS_COL + ", " + JdbcTestHelper.CREATE_COL + ", " + JdbcTestHelper.COOKIE_COL + + ", " + JdbcTestHelper.LAST_SAVE_COL + ", " + JdbcTestHelper.EXPIRY_COL + " " + ") " + + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + statement.setString(1, id); + statement.setString(2, contextPath); + statement.setString(3, vhost); + statement.setString(4, "0"); + + statement.setLong(5, System.currentTimeMillis()); + statement.setLong(6, System.currentTimeMillis()); + statement.setLong(7, System.currentTimeMillis()); + statement.setLong(8, System.currentTimeMillis()); + + statement.setLong(9, System.currentTimeMillis()); + statement.setLong(10, System.currentTimeMillis()); + + statement.execute(); + assertEquals(1, statement.getUpdateCount()); + } + } + @Test public void testLoad() throws Exception @@ -76,7 +117,7 @@ public class SessionTableSchemaTest _tableSchema.prepareTables(); //insert a fake session at the root context - JdbcTestHelper.insertSession("1234", "/", "0.0.0.0"); + insertSessionWithoutAttributes("1234", "/", "0.0.0.0"); //test if it can be seen try (Connection con = _da.getConnection()) @@ -101,7 +142,7 @@ public class SessionTableSchemaTest _tableSchema.prepareTables(); //insert a fake session at the root context - JdbcTestHelper.insertSession("1234", "/", "0.0.0.0"); + insertSessionWithoutAttributes("1234", "/", "0.0.0.0"); //test if it can be seen try (Connection con = _da.getConnection()) @@ -125,7 +166,7 @@ public class SessionTableSchemaTest _tableSchema.prepareTables(); //insert a fake session at the root context - JdbcTestHelper.insertSession("1234", "/", "0.0.0.0"); + insertSessionWithoutAttributes("1234", "/", "0.0.0.0"); //test if it can be deleted try (Connection con = _da.getConnection()) @@ -149,7 +190,7 @@ public class SessionTableSchemaTest _tableSchema.prepareTables(); //insert a fake session at the root context - JdbcTestHelper.insertSession("1234", "/", "0.0.0.0"); + insertSessionWithoutAttributes("1234", "/", "0.0.0.0"); try (Connection con = _da.getConnection()) { @@ -175,7 +216,7 @@ public class SessionTableSchemaTest _tableSchema.prepareTables(); //insert a fake session at the root context - JdbcTestHelper.insertSession("1234", "/", "0.0.0.0"); + insertSessionWithoutAttributes("1234", "/", "0.0.0.0"); try (Connection con = _da.getConnection()) { @@ -200,7 +241,7 @@ public class SessionTableSchemaTest _tableSchema.prepareTables(); //insert a fake session at the root context - JdbcTestHelper.insertSession("1234", "/", "0.0.0.0"); + insertSessionWithoutAttributes("1234", "/", "0.0.0.0"); try (Connection con = _da.getConnection()) { diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreTest.java index dc9d4ab22fa..9d8de4ff4e0 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStoreTest.java @@ -427,7 +427,72 @@ public abstract class AbstractSessionDataStoreTest //expected exception } } + + + /** + * Test that a session containing no attributes can be stored and re-read + * @throws Exception + */ + @Test + public void testEmptyLoadSession() throws Exception + { + //create the SessionDataStore + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/test"); + SessionDataStoreFactory factory = createSessionDataStoreFactory(); + ((AbstractSessionDataStoreFactory)factory).setGracePeriodSec(GRACE_PERIOD_SEC); + SessionDataStore store = factory.getSessionDataStore(context.getSessionHandler()); + SessionContext sessionContext = new SessionContext("foo", context.getServletContext()); + store.initialize(sessionContext); + store.start(); + + //persist a session that has no attributes + long now = System.currentTimeMillis(); + SessionData data = store.newSessionData("222", 100, now, now - 1, -1); + data.setLastNode(sessionContext.getWorkerName()); + //persistSession(data); + store.store("222", data); + //test that we can retrieve it + SessionData savedSession = store.load("222"); + assertEquals(0, savedSession.getAllAttributes().size()); + } + + //Test that a session that had attributes can be modified to contain no + //attributes, and still read + @Test + public void testModifyEmptyLoadSession() throws Exception + { + //create the SessionDataStore + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/test"); + SessionDataStoreFactory factory = createSessionDataStoreFactory(); + ((AbstractSessionDataStoreFactory)factory).setGracePeriodSec(GRACE_PERIOD_SEC); + SessionDataStore store = factory.getSessionDataStore(context.getSessionHandler()); + SessionContext sessionContext = new SessionContext("foo", context.getServletContext()); + store.initialize(sessionContext); + store.start(); + + //persist a session that has attributes + long now = System.currentTimeMillis(); + SessionData data = store.newSessionData("222", 100, now, now - 1, -1); + data.setAttribute("foo", "bar"); + data.setLastNode(sessionContext.getWorkerName()); + store.store("222", data); + + //test that we can retrieve it + SessionData savedSession = store.load("222"); + assertEquals("bar", savedSession.getAttribute("foo")); + + //now modify so there are no attributes + savedSession.setAttribute("foo", null); + store.store("222", savedSession); + + //check its still readable + savedSession = store.load("222"); + assertEquals(0, savedSession.getAllAttributes().size()); + } + /** * Test that we can delete a persisted session. */