All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.server.Response Maven / Gradle / Ivy

The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server;

import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ListIterator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;

import org.eclipse.jetty.http.ComplianceViolationException;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.internal.HttpChannelState;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

The representation of an HTTP response, for any protocol version (HTTP/1.1, HTTP/2, HTTP/3).

*/ public interface Response extends Content.Sink { /** * @return the {@link Request} associated with this {@code Response} */ Request getRequest(); /** * @return the response HTTP status code */ int getStatus(); /** * Set the response HTTP status code. * @param code the response HTTP status code */ void setStatus(int code); /** * @return the response HTTP headers */ HttpFields.Mutable getHeaders(); /** * @return a supplier for the HTTP trailers */ Supplier getTrailersSupplier(); /** *

Sets the supplier for the HTTP trailers.

*

The method {@link Supplier#get()} may be called by the * implementation multiple times, so it is important that * the same value is returned in every invocation.

*

Example:

*
{@code
     * // Correct usage.
     * HttpFields.Mutable trailers = HttpFields.build();
     * response.setTrailersSupplier(() -> trailers);
     *
     * // WRONG usage, as the value changes for
     * // every invocation of supplier.get().
     * response.setTrailersSupplier(() -> HttpFields.build());
     * }
* * @param trailers a supplier for the HTTP trailers */ void setTrailersSupplier(Supplier trailers); /** *

Returns whether this response has already been committed.

*

Committing a response means that the HTTP status code and HTTP headers * cannot be modified anymore, typically because they have already been * serialized and sent over the network.

* * @return whether this response has already been committed */ boolean isCommitted(); /** *

Returns whether the last write has been initiated on the response.

* * @return {@code true} if {@code last==true} has been passed to {@link #write(boolean, ByteBuffer, Callback)}. */ boolean hasLastWrite(); /** *

Returns whether the response completed successfully.

*

The response HTTP status code, HTTP headers and content * have been successfully serialized and sent over the network * without errors.

* * @return whether the response completed successfully */ boolean isCompletedSuccessfully(); /** *

Resets this response, clearing the HTTP status code, HTTP headers * and HTTP trailers.

* * @throws IllegalStateException if the response is already * {@link #isCommitted() committed} */ void reset(); /** *

Writes an {@link HttpStatus#isInterim(int) HTTP interim response}, * with the given HTTP status code and HTTP headers.

*

It is possible to write more than one interim response, for example * in case of {@link HttpStatus#EARLY_HINTS_103}.

*

The returned {@link CompletableFuture} is notified of the result * of this write, whether it succeeded or failed.

* * @param status the interim HTTP status code * @param headers the HTTP headers * @return a {@link CompletableFuture} with the result of the write */ CompletableFuture writeInterim(int status, HttpFields headers); /** * {@inheritDoc} *

The invocation of the passed {@code Callback} is serialized * with previous calls of this method, so that it is not invoked until * any invocation of the callback of a previous call to this method * has returned.

*

Thus a {@code Callback} should not block waiting for a callback * of a future call to this method.

* * @param last whether the ByteBuffer is the last to write * @param byteBuffer the ByteBuffer to write * @param callback the callback to notify when the write operation is complete */ @Override void write(boolean last, ByteBuffer byteBuffer, Callback callback); /** *

Returns a chunk processor suitable to be passed to the * {@link Content#copy(Content.Source, Content.Sink, Content.Chunk.Processor, Callback)} * method, that will handle {@link Trailers} chunks * by adding their fields to the {@link HttpFields} supplied by * {@link Response#getTrailersSupplier()}.

*

This is specifically useful for writing trailers that have been received via * the {@link Content.Source#read()} API, for example when echoing a request to a response:

*
     *   Content.copy(request, response, Response.asTrailerChunkHandler(response), callback);
     * 
* @param response The response for which to process a trailers chunk. * If the {@link Response#setTrailersSupplier(Supplier)} * method has not been called prior to this method, then a noop processor is returned. * @return A chunk processor that will add trailer chunks to the response's trailer supplied fields. * @see Content#copy(Content.Source, Content.Sink, Content.Chunk.Processor, Callback) * @see Trailers */ static Content.Chunk.Processor newTrailersChunkProcessor(Response response) { Supplier supplier = response.getTrailersSupplier(); if (supplier == null) return (chunk, callback) -> false; return (chunk, callback) -> { if (chunk instanceof Trailers trailers) { HttpFields requestTrailers = trailers.getTrailers(); if (requestTrailers != null) { // Call supplier in lambda to get latest responseTrailers HttpFields responseTrailers = supplier.get(); if (responseTrailers instanceof HttpFields.Mutable mutable) { mutable.add(requestTrailers); callback.succeeded(); return true; } } } return false; }; } /** *

Unwraps the given response, recursively, until the wrapped instance * is an instance of the given type, otherwise returns {@code null}.

* * @param response the response to unwrap * @param type the response type to find * @return the response as the given type, or {@code null} * @param the response type * @see Wrapper */ @SuppressWarnings("unchecked") static T as(Response response, Class type) { while (response != null) { if (type.isInstance(response)) return (T)response; response = response instanceof Response.Wrapper wrapper ? wrapper.getWrapped() : null; } return null; } /** *

Sends a HTTP redirect status code to the given location, * without consuming the available request content. The {@link HttpStatus#SEE_OTHER_303} * code is used, unless the request is HTTP/1.0, in which case {@link HttpStatus#MOVED_TEMPORARILY_302} is used, * unless the request is not a {@code GET} and the protocol is {@code HTTP/1.1} or later, in which case a * {@link HttpStatus#SEE_OTHER_303} is used to make the client consistently redirect with a {@code GET}. *

* @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param location the redirect location as an absolute URI or encoded relative URI path. * @see #sendRedirect(Request, Response, Callback, int, String, boolean) */ static void sendRedirect(Request request, Response response, Callback callback, String location) { sendRedirect(request, response, callback, location, false); } /** *

Sends HTTP redirect status code to the given location, * without consuming the available request content. The {@link HttpStatus#SEE_OTHER_303} * code is used, unless the request is HTTP/1.0, in which case {@link HttpStatus#MOVED_TEMPORARILY_302} is used, * unless the request is not a {@code GET} and the protocol is {@code HTTP/1.1} or later, in which case a * {@link HttpStatus#SEE_OTHER_303} is used to make the client consistently redirect with a {@code GET}. *

* @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param location the redirect location as an absolute URI or encoded relative URI path. * @param consumeAvailable whether to consumer the available request content * @see #sendRedirect(Request, Response, Callback, int, String, boolean) */ static void sendRedirect(Request request, Response response, Callback callback, String location, boolean consumeAvailable) { sendRedirect(request, response, callback, 0, location, consumeAvailable); } /** *

Sends a {@code 302} HTTP redirect status code to the given location.

* * @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param code the redirect HTTP status code * @param location the redirect location as an absolute URI or encoded relative URI path. * @param consumeAvailable whether to consumer the available request content * @see #toRedirectURI(Request, String) * @throws IllegalArgumentException if the status code is not a redirect, or the location is {@code null} * @throws IllegalStateException if the response is already {@link #isCommitted() committed} */ static void sendRedirect(Request request, Response response, Callback callback, int code, String location, boolean consumeAvailable) { sendRedirect(request, response, callback, code, location, consumeAvailable, null); } /** *

Sends a {@code 302} HTTP redirect status code to the given location.

* * @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param code the redirect HTTP status code, or 0 for a default * @param location the redirect location as an absolute URI or encoded relative URI path. * @param consumeAvailable whether to consumer the available request content * @param content the content of the response, or null for a generated HTML message if {@link HttpConfiguration#isGenerateRedirectBody()} is {@code true}. * @see #toRedirectURI(Request, String) * @throws IllegalArgumentException if the status code is not a redirect, or the location is {@code null} * @throws IllegalStateException if the response is already {@link #isCommitted() committed} */ static void sendRedirect(Request request, Response response, Callback callback, int code, String location, boolean consumeAvailable, ByteBuffer content) { if (response.isCommitted()) { callback.failed(new IllegalStateException("Committed")); return; } if (code <= 0) code = HttpMethod.GET.is(request.getMethod()) || request.getConnectionMetaData().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpStatus.MOVED_TEMPORARILY_302 : HttpStatus.SEE_OTHER_303; if (!HttpStatus.isRedirectionWithLocation(code)) { callback.failed(new IllegalArgumentException("Not a 3xx redirect code")); return; } if (location == null) { callback.failed(new IllegalArgumentException("No location")); return; } try { location = toRedirectURI(request, location); if (consumeAvailable) { while (true) { Content.Chunk chunk = response.getRequest().read(); if (chunk == null) { response.getHeaders().put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); break; } chunk.release(); if (chunk.isLast()) break; } } if (content == null && request.getConnectionMetaData().getHttpConfiguration().isGenerateRedirectBody()) { response.getHeaders().put(MimeTypes.Type.TEXT_HTML_8859_1.getContentTypeField()); String body = """ Redirecting...

If you are not redirected, click here.

""".formatted(location, location); content = BufferUtil.toBuffer(body, StandardCharsets.ISO_8859_1); } response.getHeaders().put(HttpHeader.LOCATION, location); response.setStatus(code); response.write(true, content, callback); } catch (Throwable failure) { callback.failed(failure); } } /** * Common point to generate a proper "Location" header for redirects. * * @param request the request the redirect should be based on (needed when relative locations are provided, so that * server name, scheme, port can be built out properly) * @param location the redirect location as an absolute URI or encoded relative URI path. If a relative path starts * with '/', then it is relative to the root, otherwise it is relative to the request. * @return the full redirect "Location" URL (including scheme, host, port, path, etc...) */ static String toRedirectURI(Request request, String location) { HttpConfiguration httpConfiguration = request.getConnectionMetaData().getHttpConfiguration(); // is the URI absolute already? if (!URIUtil.hasScheme(location)) { // The location is relative HttpURI uri = request.getHttpURI(); // Is it relative to the request? if (!location.startsWith("/")) { String path = uri.getPath(); String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path); location = URIUtil.addEncodedPaths(parent, location); } // Normalize out any dot dot segments location = URIUtil.normalizePathQuery(location); if (location == null) throw new IllegalStateException("redirect path cannot be above root"); // if relative redirects are not allowed? if (!httpConfiguration.isRelativeRedirectAllowed()) // make the location an absolute URI location = URIUtil.newURI(uri.getScheme(), Request.getServerName(request), Request.getServerPort(request), location, null); } UriCompliance redirectCompliance = httpConfiguration.getRedirectUriCompliance(); if (redirectCompliance != null) { String violations = UriCompliance.checkUriCompliance(redirectCompliance, HttpURI.from(location), null); if (StringUtil.isNotBlank(violations)) throw new IllegalArgumentException(violations); } return location; } /** *

Adds an HTTP {@link HttpHeader#SET_COOKIE} header to the response.

* * @param response the HTTP response * @param cookie the HTTP cookie to add * @see #putCookie(Response, HttpCookie) */ static void addCookie(Response response, HttpCookie cookie) { if (StringUtil.isBlank(cookie.getName())) throw new IllegalArgumentException("Cookie.name cannot be blank/null"); Request request = response.getRequest(); CookieCompliance compliance = request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance(); try { response.getHeaders().add(new HttpCookieUtils.SetCookieHttpField(HttpCookieUtils.checkSameSite(cookie, request.getContext()), compliance)); } catch (ComplianceViolationException e) { HttpChannel.from(request).getComplianceViolationListener().onComplianceViolation(e.getEvent()); throw e; } // Expire responses with set-cookie headers, so they do not get cached. if (!response.getHeaders().contains(HttpHeader.EXPIRES)) response.getHeaders().add(HttpFields.EXPIRES_01JAN1970); } /** *

Put a HTTP {@link HttpHeader#SET_COOKIE} header to the response.

*

If a matching {@link HttpHeader#SET_COOKIE} already exists for matching name, path, domain etc. * then it will be replaced.

* * @param response the HTTP response * @param cookie the HTTP cookie to add * @see #addCookie(Response, HttpCookie) */ static void putCookie(Response response, HttpCookie cookie) { if (StringUtil.isBlank(cookie.getName())) throw new IllegalArgumentException("Cookie.name cannot be blank/null"); Request request = response.getRequest(); HttpConfiguration httpConfiguration = request.getConnectionMetaData().getHttpConfiguration(); CookieCompliance compliance = httpConfiguration.getResponseCookieCompliance(); HttpField setCookie; try { setCookie = new HttpCookieUtils.SetCookieHttpField(HttpCookieUtils.checkSameSite(cookie, request.getContext()), compliance); } catch (ComplianceViolationException e) { HttpChannel.from(request).getComplianceViolationListener().onComplianceViolation(e.getEvent()); throw e; } boolean expires = false; for (ListIterator i = response.getHeaders().listIterator(); i.hasNext(); ) { HttpField field = i.next(); HttpHeader header = field.getHeader(); if (header == null) continue; switch (header) { case SET_COOKIE -> { if (field instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { if (!HttpCookieUtils.match(setCookieHttpField.getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } else { if (!HttpCookieUtils.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } if (setCookie == null) { i.remove(); } else { i.set(setCookie); setCookie = null; } } case EXPIRES -> expires = true; } } if (setCookie != null) response.getHeaders().add(setCookie); // Expire responses with set-cookie headers, so they do not get cached. if (!expires) response.getHeaders().add(HttpFields.EXPIRES_01JAN1970); } /** * Replace a cookie * @param response the HTTP response * @param cookie the HTTP cookie to add * @deprecated use {@link #putCookie(Response, HttpCookie)} */ @Deprecated static void replaceCookie(Response response, HttpCookie cookie) { putCookie(response, cookie); } /** *

Writes an error response with HTTP status code {@code 500}.

*

The error {@link Request.Handler} returned by {@link Context#getErrorHandler()}, * if any, is invoked.

* * @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param cause the cause of the error */ static void writeError(Request request, Response response, Callback callback, Throwable cause) { if (cause == null) cause = new Throwable("unknown cause"); int status = HttpStatus.INTERNAL_SERVER_ERROR_500; String message = cause.toString(); if (cause instanceof HttpException httpException) { status = httpException.getCode(); message = httpException.getReason(); } writeError(request, response, callback, status, message, cause); } /** *

Writes an error response with the given HTTP status code.

*

The error {@link Request.Handler} returned by {@link Context#getErrorHandler()}, * if any, is invoked.

* * @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param status the error HTTP status code */ static void writeError(Request request, Response response, Callback callback, int status) { writeError(request, response, callback, status, null, null); } /** *

Writes an error response with the given HTTP status code, * and the given message in the response content.

*

The error {@link Request.Handler} returned by {@link Context#getErrorHandler()}, * if any, is invoked.

* * @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param status the error HTTP status code * @param message the error message to write in the response content */ static void writeError(Request request, Response response, Callback callback, int status, String message) { writeError(request, response, callback, status, message, null); } /** *

Writes an error response with the given HTTP status code, * and the given message in the response content.

*

The error {@link Request.Handler} returned by {@link Context#getErrorHandler()}, * if any, is invoked.

* * @param request the HTTP request * @param response the HTTP response * @param callback the callback to complete * @param status the error HTTP status code * @param message the error message to write in the response content * @param cause the cause of the error */ static void writeError(Request request, Response response, Callback callback, int status, String message, Throwable cause) { // Retrieve the Logger instance here, rather than having a // public field that will force a transitive dependency on SLF4J. Logger logger = LoggerFactory.getLogger(Response.class); // Let's be less verbose with BadMessageExceptions & QuietExceptions if (logger.isDebugEnabled()) logger.debug("writeError: status={}, message={}, response={}", status, message, response, cause); else if (cause instanceof QuietException || cause instanceof TimeoutException) logger.debug("writeError: status={}, message={}, response={} {}", status, message, response, cause.toString()); else if (cause != null) logger.warn("writeError: status={}, message={}, response={}", status, message, response, cause); if (response.isCommitted()) { callback.failed(cause == null ? new IllegalStateException(message == null ? "Committed" : message) : cause); return; } ResponseUtils.ensureConsumeAvailableOrNotPersistent(request, response); if (status <= 0) status = HttpStatus.INTERNAL_SERVER_ERROR_500; if (message == null) message = HttpStatus.getMessage(status); response.setStatus(status); // TODO: detect recursion when an ErrorHandler calls this method, otherwise StackOverflowError. Context context = request.getContext(); Request.Handler errorHandler = context.getErrorHandler(); if (errorHandler != null) { Request errorRequest = new ErrorHandler.ErrorRequest(request, status, message, cause); try { if (errorHandler.handle(errorRequest, response, callback)) return; } catch (Throwable e) { if (cause != null && cause != e) cause.addSuppressed(e); } } // fall back to very empty error page response.getHeaders().put(ErrorHandler.ERROR_CACHE_CONTROL); response.write(true, null, callback); } /** *

Unwraps the given response until the innermost wrapped response instance.

* * @param response the response to unwrap * @return the innermost wrapped response instance * @see Wrapper */ static Response getOriginalResponse(Response response) { while (response instanceof Response.Wrapper wrapped) { response = wrapped.getWrapped(); } return response; } /** * @param response the HTTP response * @return the number of response content bytes written to the network so far, * or {@code -1} if the number is unknown */ static long getContentBytesWritten(Response response) { Response originalResponse = getOriginalResponse(response); if (originalResponse instanceof HttpChannelState.ChannelResponse channelResponse) return channelResponse.getContentBytesWritten(); return -1; } /** *

Wraps a {@link Response} as a {@link OutputStream} that performs buffering. The necessary * {@link ByteBufferPool} is taken from the request's connector while the size and direction of the buffer * is read from the request's {@link HttpConfiguration}.

*

This is equivalent to:

*

{@code Content.Sink.asOutputStream(Response.asBufferedSink(request, response))}

* @param request the request from which to get the buffering sink's settings * @param response the response to wrap * @return a buffering {@link OutputStream} */ static OutputStream asBufferedOutputStream(Request request, Response response) { return Content.Sink.asOutputStream(Response.asBufferedSink(request, response)); } /** * Wraps a {@link Response} as a {@link Content.Sink} that performs buffering. The necessary * {@link ByteBufferPool} is taken from the request's connector while the size, direction of the buffer * and commit size are read from the request's {@link HttpConfiguration}. * @param request the request from which to get the buffering sink's settings * @param response the response to wrap * @return a buffering {@link Content.Sink} */ static Content.Sink asBufferedSink(Request request, Response response) { ConnectionMetaData connectionMetaData = request.getConnectionMetaData(); ByteBufferPool bufferPool = connectionMetaData.getConnector().getByteBufferPool(); HttpConfiguration httpConfiguration = connectionMetaData.getHttpConfiguration(); int bufferSize = httpConfiguration.getOutputBufferSize(); boolean useOutputDirectByteBuffers = httpConfiguration.isUseOutputDirectByteBuffers(); int outputAggregationSize = httpConfiguration.getOutputAggregationSize(); return Content.Sink.asBuffered(response, bufferPool, useOutputDirectByteBuffers, outputAggregationSize, bufferSize); } class Wrapper implements Response { private final Request _request; private final Response _wrapped; public Wrapper(Request request, Response wrapped) { _request = request; _wrapped = wrapped; } public Response getWrapped() { return _wrapped; } @Override public Request getRequest() { return _request; } @Override public int getStatus() { return getWrapped().getStatus(); } @Override public void setStatus(int code) { getWrapped().setStatus(code); } @Override public HttpFields.Mutable getHeaders() { return getWrapped().getHeaders(); } @Override public Supplier getTrailersSupplier() { return getWrapped().getTrailersSupplier(); } @Override public void setTrailersSupplier(Supplier trailers) { getWrapped().setTrailersSupplier(trailers); } @Override public boolean isCommitted() { return getWrapped().isCommitted(); } @Override public boolean hasLastWrite() { return getWrapped().hasLastWrite(); } @Override public boolean isCompletedSuccessfully() { return getWrapped().isCompletedSuccessfully(); } @Override public void reset() { getWrapped().reset(); } @Override public CompletableFuture writeInterim(int status, HttpFields headers) { return getWrapped().writeInterim(status, headers); } @Override public void write(boolean last, ByteBuffer byteBuffer, Callback callback) { getWrapped().write(last, byteBuffer, callback); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy