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

org.eclipse.jetty.ee10.servlet.ServletChannel Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show 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.ee10.servlet;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;

import jakarta.servlet.RequestDispatcher;
import org.eclipse.jetty.ee10.servlet.ServletChannelState.Action;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.ResponseUtils;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextRequest;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.eclipse.jetty.util.thread.Invocable.InvocationType.NON_BLOCKING;

/**
 * The ServletChannel contains the state and behaviors associated with the Servlet API
 * lifecycle for a single request/response cycle. Specifically it uses
 * {@link ServletChannelState} to coordinate the states of dispatch state, input and
 * output according to the servlet specification.  The combined state so obtained
 * is reflected in the behaviour of the contained {@link HttpInput} implementation of
 * {@link jakarta.servlet.ServletInputStream}.
 * 

* This class is reusable over multiple requests for the same {@link ServletContextHandler} * and is {@link #recycle(Throwable) recycled} after each use before being * {@link #associate(ServletContextRequest) associated} with a new {@link ServletContextRequest} * and then {@link #associate(Request, Response, Callback) associated} with possibly wrapped * request, response and callback. *

* @see ServletChannelState * @see HttpInput */ public class ServletChannel { private static final Logger LOG = LoggerFactory.getLogger(ServletChannel.class); private final ServletChannelState _state; private final ServletContextHandler.ServletScopedContext _context; private final ServletContextHandler.ServletContextApi _servletContextApi; private final ConnectionMetaData _connectionMetaData; private final AtomicLong _requests = new AtomicLong(); final HttpInput _httpInput; private final HttpOutput _httpOutput; private ServletContextRequest _servletContextRequest; private Request _request; private Response _response; private Callback _callback; public ServletChannel(ServletContextHandler servletContextHandler, Request request) { this(servletContextHandler, request.getConnectionMetaData()); } public ServletChannel(ServletContextHandler servletContextHandler, ConnectionMetaData connectionMetaData) { _context = servletContextHandler.getContext(); _servletContextApi = _context.getServletContext(); _connectionMetaData = connectionMetaData; _state = new ServletChannelState(this); _httpInput = new HttpInput(this); _httpOutput = new HttpOutput(this); } public ConnectionMetaData getConnectionMetaData() { return _connectionMetaData; } public Callback getCallback() { return _callback; } /** * Associate this channel with a specific request. * This method is called by the {@link ServletContextHandler} when a core {@link Request} is accepted and associated with * a servlet mapping. The association remains functional until {@link #recycle(Throwable)} is called, * and it remains readable until a call to {@link #recycle(Throwable)} or a subsequent call to {@code associate}. * @param servletContextRequest The servlet context request to associate * @see #recycle(Throwable) */ public void associate(ServletContextRequest servletContextRequest) { _httpInput.reopen(); _request = _servletContextRequest = servletContextRequest; _response = _servletContextRequest.getServletContextResponse(); if (LOG.isDebugEnabled()) LOG.debug("associate {} -> {} : {}", this, _servletContextRequest, _state); } /** * Associate this channel with possibly wrapped values for * {@link #getRequest()}, {@link #getResponse()} and {@link #getCallback()}. * This is called by the {@link ServletHandler} immediately before calling {@link #handle()} on the * initial dispatch. This allows for handlers between the {@link ServletContextHandler} and the * {@link ServletHandler} to wrap the instances. * @param request The request, which may have been wrapped * after #{@link ServletContextHandler#wrapRequest(Request, Response)} * @param response The response, which may have been wrapped * after #{@link ServletContextHandler#wrapResponse(ContextRequest, Response)} * @param callback The context, which may have been wrapped * after {@link ServletContextHandler#handle(Request, Response, Callback)} */ public void associate(Request request, Response response, Callback callback) { if (_callback != null) throw new IllegalStateException(); if (request != _request && Request.as(request, ServletContextRequest.class) != _servletContextRequest) throw new IllegalStateException(); _request = request; _response = response; _callback = callback; _state.openOutput(); if (LOG.isDebugEnabled()) LOG.debug("associate {} -> {},{},{}", this, _request, _response, _callback); } public ServletContextHandler.ServletScopedContext getContext() { return _context; } public ServletContextHandler getServletContextHandler() { return _context.getContextHandler(); } public ServletContextHandler.ServletContextApi getServletContextApi() { return _servletContextApi; } public HttpOutput getHttpOutput() { return _httpOutput; } public HttpInput getHttpInput() { return _httpInput; } public boolean isAborted() { return _state.isAborted(); } public boolean isSendError() { return _state.isSendError(); } /** * Format the address or host returned from Request methods * * @param addr The address or host * @return Default implementation returns {@link HostPort#normalizeHost(String)} */ protected String formatAddrOrHost(String addr) { return HostPort.normalizeHost(addr); } public ServletChannelState getServletRequestState() { return _state; } private long getBytesWritten() { // This returns the bytes written to the network, // which may be different from those written by the // application as they might have been compressed. return Response.getContentBytesWritten(getServletContextResponse()); } /** * Get the idle timeout. *

This is implemented as a call to {@link EndPoint#getIdleTimeout()}, but may be * overridden by channels that have timeouts different from their connections. * * @return the idle timeout (in milliseconds) */ public long getIdleTimeout() { return _connectionMetaData.getConnection().getEndPoint().getIdleTimeout(); } /** * Set the idle timeout. *

This is implemented as a call to {@link EndPoint#setIdleTimeout(long)}, but may be * overridden by channels that have timeouts different from their connections. * * @param timeoutMs the idle timeout in milliseconds */ public void setIdleTimeout(long timeoutMs) { _connectionMetaData.getConnection().getEndPoint().setIdleTimeout(timeoutMs); } public HttpConfiguration getHttpConfiguration() { return _connectionMetaData.getHttpConfiguration(); } public Server getServer() { return _context.getContextHandler().getServer(); } /** * @return The {@link ServletContextRequest} as wrapped by the {@link ServletContextHandler}. * @see #getRequest() */ public ServletContextRequest getServletContextRequest() { return _servletContextRequest; } /** * @return The core {@link Request} associated with the request. This may differ from {@link #getServletContextRequest()} * if the request was wrapped by another handler after the {@link ServletContextHandler} and passed * to {@link ServletChannel#associate(Request, Response, Callback)}. * @see #getServletContextRequest() * @see #associate(Request, Response, Callback) */ public Request getRequest() { return _request; } /** * @return The ServetContextResponse as wrapped by the {@link ServletContextHandler}. * @see #getResponse() */ public ServletContextResponse getServletContextResponse() { ServletContextRequest request = _servletContextRequest; if (_servletContextRequest == null) throw new IllegalStateException("Request/Response does not exist (likely recycled)"); return request.getServletContextResponse(); } /** * @return The core {@link Response} associated with the API response. * This may differ from {@link #getServletContextResponse()} if the response was wrapped by another handler * after the {@link ServletContextHandler} and passed to {@link ServletChannel#associate(Request, Response, Callback)}. * @see #getServletContextResponse() * @see #associate(Request, Response, Callback) */ public Response getResponse() { if (_response == null) throw new IllegalStateException("Response does not exist (likely recycled)"); return _response; } public Connection getConnection() { return _connectionMetaData.getConnection(); } public EndPoint getEndPoint() { return getConnection().getEndPoint(); } /** *

Return the local name of the connected channel.

* *

* This is the host name after the connector is bound and the connection is accepted. *

*

* Value can be overridden by {@link HttpConfiguration#setLocalAddress(SocketAddress)}. *

*

* Note: some connectors are not based on IP networking, and default behavior here will * result in a null return. Use {@link HttpConfiguration#setLocalAddress(SocketAddress)} * to set the value to an acceptable host name. *

* * @return the local name, or null */ public String getLocalName() { InetSocketAddress local = getLocalAddress(); if (local != null) return Request.getHostName(local); return null; } /** *

Return the Local Port of the connected channel.

* *

* This is the port the connector is bound to and is accepting connections on. *

*

* Value can be overridden by {@link HttpConfiguration#setLocalAddress(SocketAddress)}. *

*

* Note: some connectors are not based on IP networking, and default behavior here will * result in a value of 0 returned. Use {@link HttpConfiguration#setLocalAddress(SocketAddress)} * to set the value to an acceptable port. *

* * @return the local port, or 0 if unspecified */ public int getLocalPort() { InetSocketAddress local = getLocalAddress(); return local == null ? 0 : local.getPort(); } public InetSocketAddress getLocalAddress() { return getRequest().getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inetSocketAddress ? inetSocketAddress : null; } public InetSocketAddress getRemoteAddress() { return getRequest().getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inetSocketAddress ? inetSocketAddress : null; } /** * Get return the HttpConfiguration server authority override. * @return return the HttpConfiguration server authority override */ public HostPort getServerAuthority() { HttpConfiguration httpConfiguration = getHttpConfiguration(); if (httpConfiguration != null) return httpConfiguration.getServerAuthority(); return null; } /** * Prepare to be reused. * @param x Any completion exception, or null for successful completion. * @see #associate(ServletContextRequest) */ void recycle(Throwable x) { // _httpInput must be recycled before _state. _httpInput.recycle(); _httpOutput.recycle(); _state.recycle(); _servletContextRequest = null; _request = null; _response = null; _callback = null; } /** * Handle the servlet request. This is called on the initial dispatch and then again on any asynchronous events. */ public void handle() { if (LOG.isDebugEnabled()) LOG.debug("handle {} {} ", _servletContextRequest.getHttpURI(), this); Action action = _state.handling(); // Loop here to handle async request redispatches. // The loop is controlled by the call to async.unhandle in the // finally block below. Unhandle will return false only if an async dispatch has // already happened when unhandle is called. loop: while (!getServer().isStopped()) { try { if (LOG.isDebugEnabled()) LOG.debug("action {} {}", action, this); switch (action) { case TERMINATED: onCompleted(); break loop; case WAIT: // break loop without calling unhandle break loop; case DISPATCH: { reopen(); dispatch(); break; } case ASYNC_DISPATCH: { reopen(); dispatchAsync(); break; } case ASYNC_TIMEOUT: _state.onTimeout(); break; case SEND_ERROR: { Object errorException = _servletContextRequest.getAttribute((RequestDispatcher.ERROR_EXCEPTION)); Throwable cause = errorException instanceof Throwable throwable ? throwable : null; try { // Get ready to send an error response getServletContextResponse().resetContent(); // the following is needed as you cannot trust the response code and reason // as those could have been modified after calling sendError Integer code = (Integer)_servletContextRequest.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); if (code == null) code = HttpStatus.INTERNAL_SERVER_ERROR_500; getServletContextResponse().setStatus(code); // The handling of the original dispatch failed, and we are now going to either generate // and error response ourselves or dispatch for an error page. If there is content left over // from the failed dispatch, then we try to consume it here and if we fail we add a // Connection:close. This can't be deferred to COMPLETE as the response will be committed // by then. if (!_httpInput.consumeAvailable()) ResponseUtils.ensureNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse()); ContextHandler.ScopedContext context = (ContextHandler.ScopedContext)_servletContextRequest.getAttribute(ErrorHandler.ERROR_CONTEXT); Request.Handler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); // If we can't have a body or have no ErrorHandler, then create a minimal error response. if (HttpStatus.hasNoBody(getServletContextResponse().getStatus()) || errorHandler == null) { sendErrorResponseAndComplete(); } else { // We do not notify ServletRequestListener on this dispatch because it might not // be dispatched to an error page, so we delegate this responsibility to the ErrorHandler. reopen(); _state.errorHandling(); // TODO We currently directly call the errorHandler here, but this is not correct in the case of async errors, // because since a failure has already occurred, the errorHandler is unable to write a response. // Instead, we should fail the callback, so that it calls Response.writeError(...) with an ErrorResponse // that ignores existing failures. However, the error handler needs to be able to call servlet pages, // so it will need to do a new call to associate(req,res,callback) or similar, to make the servlet request and // response wrap the error request and response. Have to think about what callback is passed. errorHandler.handle(getServletContextRequest(), getServletContextResponse(), Callback.from(() -> _state.errorHandlingComplete(null), _state::errorHandlingComplete)); } } catch (Throwable x) { if (cause == null) cause = x; else ExceptionUtil.addSuppressedIfNotAssociated(cause, x); if (LOG.isDebugEnabled()) LOG.debug("Could not perform error handling, aborting", cause); try { if (_state.isResponseCommitted()) { // Perform the same behavior as when the callback is failed. _state.errorHandlingComplete(cause); } else { getServletContextResponse().resetContent(); sendErrorResponseAndComplete(); } } catch (Throwable t) { ExceptionUtil.addSuppressedIfNotAssociated(t, cause); abort(t); } } finally { // clean up the context that was set in Response.sendError _servletContextRequest.removeAttribute(ErrorHandler.ERROR_CONTEXT); } break; } case ASYNC_ERROR: { throw _state.getAsyncContextEvent().getThrowable(); } case READ_CALLBACK: { _context.run(() -> _servletContextRequest.getHttpInput().run()); break; } case WRITE_CALLBACK: { _context.run(() -> _servletContextRequest.getHttpOutput().run()); break; } case COMPLETE: { ServletContextResponse response = getServletContextResponse(); if (!response.isCommitted()) { // Indicate Connection:close if we can't consume all. if (response.getStatus() >= 200) ResponseUtils.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, response); } // RFC 7230, section 3.3. We do this here so that a servlet error page can be sent. if (!_servletContextRequest.isHead() && response.getStatus() != HttpStatus.NOT_MODIFIED_304) { // Compare the bytes written by the application, even if // they might be compressed (or changed) by child Handlers. long written = response.getContentBytesWritten(); if (response.isContentIncomplete(written)) { sendErrorOrAbort("Insufficient content written %d < %d".formatted(written, response.getContentLength())); break; } } // Set a close callback on the HttpOutput to make it an async callback response.completeOutput(Callback.from(NON_BLOCKING, () -> _state.completed(null), _state::completed)); break; } default: throw new IllegalStateException(this.toString()); } } catch (Throwable failure) { if ("org.eclipse.jetty.continuation.ContinuationThrowable".equals(failure.getClass().getName())) LOG.trace("IGNORED", failure); else handleException(failure); } action = _state.unhandle(); } if (LOG.isDebugEnabled()) LOG.debug("!handle {} {}", action, this); } private void reopen() { _servletContextRequest.getServletContextResponse().getHttpOutput().reopen(); getHttpOutput().reopen(); } /** * @param message the error message. * @return true if we have sent an error, false if we have aborted. */ private boolean sendErrorOrAbort(String message) { try { if (isCommitted()) { abort(new IOException(message)); return false; } getServletContextResponse().getServletApiResponse().sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); return true; } catch (Throwable x) { LOG.trace("IGNORED", x); abort(x); } return false; } /** *

Sends an error 500, performing a special logic to detect whether the request is suspended, * to avoid concurrent writes from the application.

*

It may happen that the application suspends, and then throws an exception, while an application * spawned thread writes the response content; in such case, we attempt to commit the error directly * bypassing the {@link ErrorHandler} mechanisms and the response OutputStream.

* * @param failure the Throwable that caused the problem */ protected void handleException(Throwable failure) { // Unwrap wrapping Jetty and Servlet exceptions. Throwable quiet = unwrap(failure, QuietException.class); Throwable noStack = unwrap(failure, BadMessageException.class, IOException.class, TimeoutException.class); if (quiet != null || !getServer().isRunning()) { if (LOG.isDebugEnabled()) LOG.debug(_servletContextRequest.getServletApiRequest().getRequestURI(), failure); } else if (noStack != null) { // No stack trace unless there is debug turned on if (LOG.isDebugEnabled()) LOG.warn("handleException {}", _servletContextRequest.getServletApiRequest().getRequestURI(), failure); else LOG.warn("handleException {} {}", _servletContextRequest.getServletApiRequest().getRequestURI(), noStack.toString()); } else { ServletContextRequest request = _servletContextRequest; LOG.warn(request == null ? "unknown request" : request.getServletApiRequest().getRequestURI(), failure); } if (isCommitted()) { abort(failure); } else { try { _state.onError(failure); } catch (IllegalStateException e) { abort(failure); } } } /** * Unwrap failure causes to find target class * * @param failure The throwable to have its causes unwrapped * @param targets Exception classes that we should not unwrap * @return A target throwable or null */ protected Throwable unwrap(Throwable failure, Class... targets) { while (failure != null) { for (Class x : targets) { if (x.isInstance(failure)) return failure; } failure = failure.getCause(); } return null; } public void sendErrorResponseAndComplete() { try { _state.completing(); getServletContextResponse().write(true, getServletContextResponse().getHttpOutput().getByteBuffer(), Callback.from(() -> _state.completed(null), _state::completed)); } catch (Throwable x) { abort(x); _state.completed(x); } } @Override public String toString() { if (_servletContextRequest == null) { return String.format("%s@%x{null}", getClass().getSimpleName(), hashCode()); } long timeStamp = Request.getTimeStamp(_servletContextRequest); return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}", getClass().getSimpleName(), hashCode(), _state, _requests, isRequestCompleted(), isResponseCompleted(), _state.getState(), _servletContextRequest.getHttpURI(), timeStamp == 0 ? 0 : System.currentTimeMillis() - timeStamp); } void onTrailers(HttpFields trailers) { _servletContextRequest.setTrailers(trailers); } /** * @see #abort(Throwable) */ public void onCompleted() { ServletApiRequest apiRequest = _servletContextRequest.getServletApiRequest(); if (LOG.isDebugEnabled()) LOG.debug("onCompleted for {} written app={} net={}", apiRequest.getRequestURI(), getHttpOutput().getWritten(), getBytesWritten()); if (getServer().getRequestLog() instanceof CustomRequestLog) { CustomRequestLog.LogDetail logDetail = new CustomRequestLog.LogDetail( _servletContextRequest.getServletName(), apiRequest.getServletContext().getRealPath(Request.getPathInContext(_servletContextRequest))); _servletContextRequest.setAttribute(CustomRequestLog.LOG_DETAIL, logDetail); } // Callback is completed only here. Callback callback = _callback; Throwable failure = _state.completeResponse(); if (failure == null) callback.succeeded(); else callback.failed(failure); } public boolean isCommitted() { return _state.isResponseCommitted(); } /** * @return True if the request lifecycle is completed */ public boolean isRequestCompleted() { return _state.isCompleted(); } /** * @return True if the response is completely written. */ public boolean isResponseCompleted() { return _state.isResponseCompleted(); } protected void execute(Runnable task) { _context.execute(task); } protected void execute(Runnable task, Request request) { _context.execute(task, request); } /** * If a write or similar operation to this channel fails, * then this method should be called. * * @param failure the failure that caused the abort. * @see #onCompleted() */ public void abort(Throwable failure) { // Callback will be failed in onCompleted(). _state.abort(failure); } private void dispatch() throws Exception { ServletContextHandler servletContextHandler = getServletContextHandler(); ServletContextRequest servletContextRequest = getServletContextRequest(); ServletApiRequest servletApiRequest = servletContextRequest.getServletApiRequest(); try { servletContextHandler.requestInitialized(servletContextRequest, servletApiRequest); ServletHandler servletHandler = servletContextHandler.getServletHandler(); ServletHandler.MappedServlet mappedServlet = servletContextRequest.getMatchedResource().getResource(); mappedServlet.handle(servletHandler, Request.getPathInContext(servletContextRequest), servletApiRequest, servletContextRequest.getHttpServletResponse()); } finally { servletContextHandler.requestDestroyed(servletContextRequest, servletApiRequest); } } public void dispatchAsync() throws Exception { ServletContextHandler servletContextHandler = getServletContextHandler(); ServletContextRequest servletContextRequest = getServletContextRequest(); ServletApiRequest servletApiRequest = servletContextRequest.getServletApiRequest(); try { servletContextHandler.requestInitialized(servletContextRequest, servletApiRequest); HttpURI uri; String pathInContext; AsyncContextEvent asyncContextEvent = _state.getAsyncContextEvent(); String dispatchString = asyncContextEvent.getDispatchPath(); if (dispatchString != null) { String contextPath = _context.getContextPath(); HttpURI.Immutable dispatchUri = HttpURI.from(dispatchString); pathInContext = URIUtil.canonicalPath(dispatchUri.getPath()); uri = HttpURI.build(servletContextRequest.getHttpURI()) .path(URIUtil.addPaths(contextPath, pathInContext)) .query(dispatchUri.getQuery()); } else { uri = asyncContextEvent.getBaseURI(); if (uri == null) { uri = servletContextRequest.getHttpURI(); pathInContext = Request.getPathInContext(servletContextRequest); } else { pathInContext = uri.getCanonicalPath(); int length = _context.getContextPath().length(); if (length > 1) pathInContext = pathInContext.substring(length); } } // We first worked with the core pathInContext above, but now need to convert to servlet style String decodedPathInContext = URIUtil.decodePath(pathInContext); Dispatcher dispatcher = new Dispatcher(servletContextHandler, uri, decodedPathInContext); dispatcher.async(asyncContextEvent.getSuppliedRequest(), asyncContextEvent.getSuppliedResponse()); } finally { servletContextHandler.requestDestroyed(servletContextRequest, servletApiRequest); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy