001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import com.google.errorprone.annotations.CanIgnoreReturnValue;
020import com.google.errorprone.annotations.CheckReturnValue;
021import com.soklet.internal.mcp.protocol.McpServerRuntimeBridge.SimulationSession;
022import com.soklet.internal.microhttp.StreamLifecycleCoordinator;
023
024import com.soklet.SseRequestResult.HandshakeAccepted;
025import com.soklet.SseRequestResult.HandshakeRejected;
026import com.soklet.annotation.SseEventSource;
027import com.soklet.exception.IllegalRequestException;
028import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
029import com.soklet.internal.streaming.ManagedResponseStream;
030import com.soklet.internal.streaming.ManagedSseLifecycle;
031import com.soklet.internal.streaming.PublisherResponseStream;
032import org.jspecify.annotations.NonNull;
033import org.jspecify.annotations.Nullable;
034
035import javax.annotation.concurrent.NotThreadSafe;
036import javax.annotation.concurrent.ThreadSafe;
037import java.io.ByteArrayOutputStream;
038import java.io.IOException;
039import java.io.Reader;
040import java.lang.reflect.InvocationTargetException;
041import java.nio.ByteBuffer;
042import java.nio.CharBuffer;
043import java.nio.charset.Charset;
044import java.nio.charset.CharsetEncoder;
045import java.nio.charset.CharacterCodingException;
046import java.nio.charset.CoderResult;
047import java.nio.charset.StandardCharsets;
048import java.time.Duration;
049import java.time.Instant;
050import java.util.ArrayList;
051import java.util.Collections;
052import java.util.EnumSet;
053import java.util.HashMap;
054import java.util.LinkedHashMap;
055import java.util.List;
056import java.util.LinkedHashSet;
057import java.util.HashSet;
058import java.util.Locale;
059import java.util.Map;
060import java.util.Map.Entry;
061import java.util.Objects;
062import java.util.Optional;
063import java.util.Set;
064import java.util.concurrent.ConcurrentHashMap;
065import java.util.concurrent.CountDownLatch;
066import java.util.concurrent.RejectedExecutionException;
067import java.util.concurrent.CompletionStage;
068import java.util.concurrent.TimeUnit;
069import java.util.concurrent.atomic.AtomicBoolean;
070import java.util.concurrent.atomic.AtomicReference;
071import java.util.concurrent.locks.ReentrantLock;
072import java.util.function.BiConsumer;
073import java.util.function.Consumer;
074import java.util.function.Function;
075import java.util.function.Supplier;
076import java.util.stream.Collectors;
077
078import static com.soklet.internal.ObjectIdentity.sameInstance;
079import static com.soklet.Utilities.emptyByteArray;
080import static java.lang.String.format;
081import static java.util.Objects.requireNonNull;
082
083/**
084 * Owns one ready-to-terminal Soklet lifecycle and its configured HTTP, SSE,
085 * and MCP transports. {@link #run(SokletConfig)} returns
086 * after all configured transports reach readiness; each instance is one-shot.
087 * Every run registers a JVM shutdown hook by default. An embedding host can
088 * disable only that registration through
089 * {@link SokletConfig.Builder#jvmShutdownHookEnabled(Boolean)}. Bounded application
090 * cleanup, terminal reporting, and process-registration release remain owned
091 * by the instance until finalization finishes, even when nobody waits.
092 * <p>
093 * <pre>{@code
094 * SokletConfig config = SokletConfig.withHttpServer(HttpServer.fromPort(8080)).build();
095 * Soklet soklet = Soklet.run(config);
096 * ShutdownResult result = soklet.awaitShutdown();
097 * }</pre>
098 * To stop programmatically, call {@link #requestShutdown()} and then await
099 * completion from an independent owning thread. An embedding host must finish
100 * Soklet shutdown before closing shared databases, telemetry, or logging.
101 * JVM hooks execute concurrently and do not establish resource ordering.
102 * With the automatic hook disabled, the host must prevent new runs once
103 * termination begins and request and synchronously await shutdown before its
104 * own shutdown hook returns. Any deregistration or delay belongs before that
105 * shutdown request; shared dependencies remain available until shutdown succeeds.
106 * <p>
107 * Required application cleanup belongs in bounded {@link ShutdownCleanup},
108 * rather than post-wait code or observational lifecycle callbacks: during JVM
109 * shutdown the JVM need not let those callers finish. Cleanup is eligible only
110 * after complete core shutdown; a cleanup timeout does not forcibly stop its code.
111 * Running built-in listeners and the lifecycle owner retain JVM liveness until
112 * shutdown and finalization; custom transports own their listener-thread policy.
113 * <p>
114 * Soklet also offers an off-network {@link Simulator} through
115 * {@link SokletSimulator}, useful for integration testing.
116 * <p>
117 * Given a <em>Resource Method</em>...
118 * <pre>{@code public class HelloResource {
119 *   @GET("/hello")
120 *   public String hello(@QueryParameter String name) {
121 *     return String.format("Hello, %s", name);
122 *   }
123 * }}</pre>
124 * ...we might test it like this:
125 * <pre>{@code @Test
126 * public void integrationTest() {
127 *   // Instead of running on a real HTTP server that listens on a port,
128 *   // a non-network Simulator is provided against which you can
129 *   // issue requests and receive responses.
130 *   SokletSimulator.run(SimulatorConfig.builder().httpServer().build(), simulator -> {
131 *     // Construct a request
132 *     Request request = Request.withPath(HttpMethod.GET, "/hello")
133 *       .queryParameters(Map.of("name", List.of("Mark")))
134 *       .build();
135 *
136 *     // Perform the request and get a handle to the response
137 *     HttpRequestResult result = simulator.performHttpRequest(request);
138 *     MarshaledResponse marshaledResponse = result.getMarshaledResponse();
139 *
140 *     // Verify status code
141 *     Integer expectedCode = 200;
142 *     Integer actualCode = marshaledResponse.getStatusCode();
143 *     assertEquals(expectedCode, actualCode, "Bad status code");
144 *
145 *     // Verify response body
146 *     marshaledResponse.getBody().ifPresentOrElse(body -> {
147 *       String expectedBody = "Hello, Mark";
148 *       byte[] bytes = ((MarshaledResponseBody.Bytes) body).getBytes();
149 *       String actualBody = new String(bytes, StandardCharsets.UTF_8);
150 *       assertEquals(expectedBody, actualBody, "Bad response body");
151 *     }, () -> {
152 *       Assertions.fail("No response body");
153 *     });
154 *   });
155 * }}</pre>
156 * <p>
157 * The {@link Simulator} also supports Server-Sent Events.
158 * <p>
159 * Integration testing documentation is available at <a href="https://www.soklet.com/docs/testing">https://www.soklet.com/docs/testing</a>.
160 *
161 * @author <a href="https://www.revetkn.com">Mark Allen</a>
162 */
163@ThreadSafe
164public final class Soklet {
165        /**
166         * Application work whose return or failure ends one running Soklet scope.
167         * The action runs once, on the calling thread after readiness, without a
168         * lifecycle handle. Checked failures retain their application-selected type.
169         * The action itself has no imposed timeout and is not interrupted by Soklet
170         * when a transport stops or JVM shutdown begins.
171         *
172         * @param <E> throwable type selected by the action
173         */
174        @NotThreadSafe
175        @FunctionalInterface
176        public interface RunAction<E extends @NonNull Throwable> {
177                /**
178                 * Performs work while the enclosing caller owns the running application.
179                 *
180                 * @throws E if application work fails
181                 */
182                void run() throws E;
183        }
184
185        @NonNull
186        private static final Map<@NonNull String, @NonNull List<@NonNull String>> DEFAULT_ACCEPTED_HANDSHAKE_HEADERS;
187
188        static {
189                // Generally speaking, we always want these headers for SSE streaming responses.
190                // Users can override if they think necessary
191                LinkedCaseInsensitiveMap<List<String>> defaultAcceptedHandshakeHeaders = new LinkedCaseInsensitiveMap<>(4);
192                defaultAcceptedHandshakeHeaders.put("Content-Type", List.of("text/event-stream; charset=UTF-8"));
193                defaultAcceptedHandshakeHeaders.put("Cache-Control", List.of("no-cache", "no-transform"));
194                defaultAcceptedHandshakeHeaders.put("Connection", List.of("keep-alive"));
195                defaultAcceptedHandshakeHeaders.put("X-Accel-Buffering", List.of("no"));
196
197                DEFAULT_ACCEPTED_HANDSHAKE_HEADERS = Collections.unmodifiableMap(defaultAcceptedHandshakeHeaders);
198        }
199
200        /**
201         * Constructs an internal lifecycle for transport-isolated simulation and
202         * low-level lifecycle composition. Claims transport identities immediately.
203         *
204         * @param sokletConfig configuration that drives the Soklet system
205         * @return a Soklet instance
206         * @throws TransportOwnershipException if a configured transport identity
207         * has already been claimed by another lifecycle
208         */
209        @NonNull
210        static Soklet fromConfig(@NonNull SokletConfig sokletConfig) {
211                requireNonNull(sokletConfig);
212                return new Soklet(sokletConfig, LifecycleRuntimeServices.system(),
213                                ignored -> { });
214        }
215
216        @NonNull
217        static Soklet fromConfig(@NonNull SokletConfig sokletConfig,
218                        @NonNull LifecycleRuntimeServices services,
219                        @NonNull Consumer<@NonNull InternalLifecycleCoreSnapshot> coreSnapshotPublisher) {
220                return new Soklet(requireNonNull(sokletConfig), requireNonNull(services),
221                                requireNonNull(coreSnapshotPublisher));
222        }
223
224        @NonNull
225        private final SokletConfig sokletConfig;
226        @NonNull
227        private final ReentrantLock lock;
228        @NonNull
229        private final CountDownLatch awaitShutdownLatch;
230        @NonNull
231        private final AtomicReference<@NonNull CountDownLatch>
232                        awaitShutdownLatchReference;
233        @NonNull
234        private final SokletFrameworkSetup frameworkSetup;
235        @NonNull
236        private final SokletDirectLifecycle directLifecycle;
237        @NonNull
238        private final CompletionStage<@NonNull ShutdownResult> coreAwaitStage;
239        private volatile @Nullable SokletRunCompletion runCompletion;
240
241        /**
242         * Creates a Soklet instance with the given configuration.
243         *
244         * @param sokletConfig configuration that drives the Soklet system
245         */
246        private Soklet(@NonNull SokletConfig sokletConfig,
247                        @NonNull LifecycleRuntimeServices services,
248                        @NonNull Consumer<@NonNull InternalLifecycleCoreSnapshot> coreSnapshotPublisher) {
249                requireNonNull(sokletConfig);
250
251                this.sokletConfig = sokletConfig;
252                this.lock = new ReentrantLock();
253                this.awaitShutdownLatch = new CountDownLatch(1);
254                this.awaitShutdownLatchReference =
255                                new AtomicReference<>(this.awaitShutdownLatch);
256                this.frameworkSetup = new SokletFrameworkSetup(sokletConfig);
257                this.directLifecycle = new SokletDirectLifecycle(this, sokletConfig,
258                                this.frameworkSetup, requireNonNull(services),
259                                requireNonNull(coreSnapshotPublisher));
260                this.coreAwaitStage = this.directLifecycle.shutdownStage().thenApply(result -> {
261                        this.directLifecycle.throwIfUnsuccessfulShutdown(result);
262                        return result;
263                });
264        }
265
266        /**
267         * Starts one lifecycle, returning only after every configured transport has
268         * reached readiness. By default owns a JVM shutdown hook until full finalization;
269         * {@link SokletConfig.Builder#jvmShutdownHookEnabled(Boolean)} may disable only
270         * that automatic registration. Programmatic shutdown, cleanup, and lifecycle
271         * owner liveness remain unchanged. The host then owns signal-driven shutdown
272         * and must request and synchronously await it before its own hook returns.
273         * Startup failure or cancellation throws
274         * synchronously after attempted teardown; interruption before readiness
275         * requests cancellation and the caller's interrupt status is restored.
276         * Cancellation competes atomically with readiness. If readiness wins, the
277         * ready handle is returned with the interrupt status restored, and that
278         * startup interruption does not request shutdown of the running lifecycle.
279         *
280         * @param sokletConfig configuration with unclaimed transport identities
281         * @return the ready lifecycle handle
282         * @throws TransportOwnershipException if a configured transport is already claimed
283         * @throws SokletStartupException if readiness is not reached
284         */
285        @NonNull
286        public static Soklet run(@NonNull SokletConfig sokletConfig) {
287                return SokletProcessLifecycle.launch(requireNonNull(sokletConfig), null);
288        }
289
290        /**
291         * Starts a lifecycle with bounded application cleanup eligible after complete
292         * core shutdown. All arguments are validated before transports are claimed.
293         * When enabled, the JVM hook joins bounded core shutdown, cleanup, and reporting.
294         * Both await methods additionally observe process-registration release.
295         *
296         * @param sokletConfig configuration with unclaimed transport identities
297         * @param cleanup bounded synchronous application cleanup
298         * @return the ready lifecycle handle
299         * @throws TransportOwnershipException if a configured transport is already claimed
300         * @throws SokletStartupException if readiness is not reached
301         */
302        @NonNull
303        public static Soklet run(@NonNull SokletConfig sokletConfig,
304                        @NonNull ShutdownCleanup cleanup) {
305                return SokletProcessLifecycle.launch(requireNonNull(sokletConfig),
306                                requireNonNull(cleanup));
307        }
308
309        /**
310         * Starts Soklet, invokes the action after readiness, and requests shutdown
311         * when the action returns or throws. Joins full finalization before returning
312         * the immutable terminal result or propagating a failure. The action runs
313         * once on the calling thread; startup failure prevents its invocation.
314         * All arguments are validated before transports are claimed.
315         * The configuration's {@link SokletConfig#isJvmShutdownHookEnabled()} option
316         * applies to scoped execution as well; disabling the automatic hook does not
317         * disable teardown when the action returns or throws.
318         * <p>
319         * A callback failure remains the exact primary exception or Error. A
320         * concurrent finalization failure is suppressed on it using ordinary Java
321         * suppression semantics, after finalization has finished. Without a callback
322         * failure, finalization uses the same failure classification as
323         * {@link #awaitShutdown()}. A complete forced shutdown can return normally.
324         * <p>
325         * Finalization does not abandon shutdown when the caller is interrupted.
326         * An interrupt flag left set by the action is temporarily cleared for the
327         * join, and it and interruptions received while joining are restored before
328         * returning or throwing. An InterruptedException thrown by the action is
329         * propagated as that action's original failure.
330         * <p>
331         * The action has no imposed timeout. An external shutdown does not terminate
332         * it; the scoped invocation still needs the action to return or throw.
333         *
334         * @param sokletConfig configuration with unclaimed transport identities
335         * @param action work defining the scope's lifetime
336         * @param <E> throwable type selected by the action
337         * @return the immutable successful terminal result; it may be ignored
338         * @throws E if the action fails, after attempted shutdown and full finalization
339         * @throws SokletStartupException if startup does not reach readiness
340         * @throws SokletLifecycleException if finalization fails without an action failure
341         */
342        @NonNull
343        public static <E extends @NonNull Throwable> ShutdownResult run(
344                        @NonNull SokletConfig sokletConfig,
345                        @NonNull RunAction<@NonNull E> action) throws E {
346                return runScoped(sokletConfig, null, action);
347        }
348
349        /**
350         * Runs one action-defined scope with bounded application cleanup. Shares the
351         * execution, error, and interruption semantics of
352         * {@link #run(SokletConfig, RunAction)}. Cleanup is eligible only after
353         * complete core shutdown, including when the action failed; incomplete core
354         * shutdown skips cleanup. All arguments are validated before transport claims.
355         *
356         * @param sokletConfig configuration with unclaimed transport identities
357         * @param cleanup bounded synchronous application cleanup
358         * @param action work defining the scope's lifetime
359         * @param <E> throwable type selected by the action
360         * @return the immutable successful terminal result; it may be ignored
361         * @throws E if the action fails, after attempted shutdown and full finalization
362         * @throws SokletStartupException if startup does not reach readiness
363         * @throws SokletLifecycleException if finalization fails without an action failure
364         */
365        @NonNull
366        public static <E extends @NonNull Throwable> ShutdownResult run(
367                        @NonNull SokletConfig sokletConfig,
368                        @NonNull ShutdownCleanup cleanup,
369                        @NonNull RunAction<@NonNull E> action) throws E {
370                return runScoped(sokletConfig, requireNonNull(cleanup), action);
371        }
372
373        @NonNull
374        private static <E extends @NonNull Throwable> ShutdownResult runScoped(
375                        @NonNull SokletConfig sokletConfig, @Nullable ShutdownCleanup cleanup,
376                        @NonNull RunAction<E> action) throws E {
377                SokletConfig exactConfig = requireNonNull(sokletConfig);
378                RunAction<E> exactAction = requireNonNull(action);
379                Soklet soklet = SokletProcessLifecycle.launch(exactConfig, cleanup);
380                Throwable actionFailure = null;
381                try {
382                        exactAction.run();
383                } catch (Throwable failure) {
384                        actionFailure = failure;
385                }
386
387                boolean interrupted = Thread.interrupted();
388                Throwable requestFailure = null;
389                Throwable finalizationFailure = null;
390                ShutdownResult result = null;
391                try {
392                        try {
393                                soklet.requestShutdown();
394                        } catch (Throwable failure) {
395                                requestFailure = failure;
396                        }
397                        for (;;) {
398                                try {
399                                        result = soklet.awaitShutdown();
400                                        break;
401                                } catch (InterruptedException interruption) {
402                                        interrupted = true;
403                                } catch (Throwable failure) {
404                                        finalizationFailure = failure;
405                                        break;
406                                }
407                        }
408                } finally {
409                        if (interrupted)
410                                Thread.currentThread().interrupt();
411                }
412
413                // Never acquire an application exception's monitor until the lifecycle
414                // has finished, and never combine failures while holding lifecycle locks.
415                if (finalizationFailure == null)
416                        finalizationFailure = requestFailure;
417                else if (requestFailure != null && !sameInstance(finalizationFailure, requestFailure))
418                        finalizationFailure.addSuppressed(requestFailure);
419                if (actionFailure != null) {
420                        if (finalizationFailure != null && !sameInstance(actionFailure, finalizationFailure))
421                                actionFailure.addSuppressed(finalizationFailure);
422                        Soklet.<E>throwRunFailure(actionFailure);
423                }
424                if (finalizationFailure != null)
425                        Soklet.<E>throwRunFailure(finalizationFailure);
426                return requireNonNull(result);
427        }
428
429        @SuppressWarnings("unchecked")
430        private static <E extends @NonNull Throwable> void throwRunFailure(
431                        @NonNull Throwable failure) throws E {
432                throw (E) requireNonNull(failure);
433        }
434
435        /**
436         * Requests shutdown promptly without waiting. Repeated requests are safe.
437         * Handlers and callbacks may request shutdown but must return without waiting
438         * for work whose termination depends on their own return.
439         */
440        public void requestShutdown() {
441                this.directLifecycle.requestShutdownIntent();
442        }
443
444        /**
445         * Claims this one-shot lifecycle's sole start attempt and returns only after
446         * every configured transport is globally ready.
447         * If the calling thread is already interrupted, startup is canceled before
448         * readiness and fails with {@link SokletStartupException}; its lifecycle
449         * result records {@link StartupDisposition#CANCELED}.
450         *
451         * @throws SokletStartupException if startup ends before readiness, carrying
452         * the exact immutable lifecycle result
453         * @throws IllegalStateException if another start call already claimed this
454         * lifecycle attempt
455         */
456        void start() {
457                this.directLifecycle.start();
458        }
459
460        /**
461         * Publishes Soklet-wide shutdown intent promptly and returns the one cached,
462         * read-only completion stage for this lifecycle attempt.
463         * <p>
464         * This method does not install or own process hooks, operating-system signal
465         * handlers, or standard-input listeners; direct lifecycle callers own those
466         * integrations.
467         * <p>
468         * Handlers, lifecycle/metric callbacks and transport-owned work may request
469         * shutdown here, but must return without joining this stage or waiting on a
470         * future derived from it. Shutdown can be waiting for that same work to exit;
471         * a self-wait can consume the shutdown budget and publish an incomplete result.
472         * Wait for completion from the owning process or another independent thread.
473         *
474         * @return cached shutdown-result stage
475         */
476        @NonNull
477        CompletionStage<@NonNull ShutdownResult> shutdown() {
478                return this.directLifecycle.shutdown();
479        }
480
481        /**
482         * Waits for full shutdown, bounded application cleanup, terminal reporting,
483         * and process-registration release, without requesting shutdown. This wait
484         * and {@link #awaitShutdownAsync()} expose the same immutable result or underlying
485         * lifecycle failure; the asynchronous stage wraps failures in
486         * {@link java.util.concurrent.CompletionException}.
487         *
488         * @return the exact immutable terminal result
489         * @throws InterruptedException if this wait is interrupted; the lifecycle continues
490         * @throws SokletLifecycleException if the lifecycle or eligible cleanup fails
491         * @throws IllegalStateException if tracked lifecycle work attempts to join itself
492         */
493        @NonNull
494        @CanIgnoreReturnValue
495        public ShutdownResult awaitShutdown() throws InterruptedException {
496                SokletRunCompletion completion = this.runCompletion;
497                if (completion != null)
498                        return completion.await();
499                ShutdownResult result = awaitCoreShutdown();
500                this.directLifecycle.throwIfUnsuccessfulShutdown(result);
501                return result;
502        }
503
504        /**
505         * Observes the same full terminal outcome as {@link #awaitShutdown()} without
506         * blocking or requesting shutdown. Returns one cached read-only stage; no
507         * waiting thread is created by this method. Lifecycle failures complete it
508         * exceptionally, with the underlying lifecycle failure wrapped in
509         * {@link java.util.concurrent.CompletionException}. Mutating a future derived
510         * from it cannot alter the lifecycle.
511         * Callbacks must not join this stage from work that shutdown must drain.
512         * The daemon completion handoff does not keep the JVM alive, including during
513         * normal programmatic shutdown after {@code main} returns. A host composing
514         * asynchronous cleanup must retain process liveness until its dependent stages
515         * finish. Put required standalone application cleanup in {@link ShutdownCleanup}.
516         *
517         * @return cached full-finalization completion stage
518         */
519        @NonNull
520        @CheckReturnValue
521        public CompletionStage<@NonNull ShutdownResult> awaitShutdownAsync() {
522                SokletRunCompletion completion = this.runCompletion;
523                return completion == null ? this.coreAwaitStage : completion.async();
524        }
525
526        @NonNull
527        ShutdownResult awaitCoreShutdown() throws InterruptedException {
528                return this.directLifecycle.awaitPublicCompletion();
529        }
530
531        void attachRunCompletion(@NonNull SokletRunCompletion completion) {
532                this.runCompletion = requireNonNull(completion);
533        }
534
535        /**
536         * Nonpublic "informal" implementation of {@link com.soklet.HttpServer.RequestHandler} so Soklet does not need to expose {@code handleRequest} publicly.
537         * Reasoning: users of this library should never call {@code handleRequest} directly - it should only be invoked in response to events
538         * provided by a {@link HttpServer} or {@link SseServer} implementation.
539         */
540        void handleRequest(@NonNull Request request,
541                                                                                                                 @NonNull ServerType serverType,
542                                                                                                                 @NonNull Consumer<@NonNull HttpRequestResult> requestResultConsumer) {
543                try (LifecycleExecutionContext.Scope ignored =
544                                         this.directLifecycle.enterExecution()) {
545                        handleRequestWithinLifecycle(request, serverType,
546                                        requestResultConsumer);
547                }
548        }
549
550        private void handleRequestWithinLifecycle(@NonNull Request request,
551                                                                                                                 @NonNull ServerType serverType,
552                                                                                                                 @NonNull Consumer<@NonNull HttpRequestResult> requestResultConsumer) {
553                requireNonNull(request);
554                requireNonNull(serverType);
555                requireNonNull(requestResultConsumer);
556
557                long processingStartedNanos = System.nanoTime();
558
559                SokletConfig sokletConfig = getSokletConfig();
560                ResourceMethodResolver resourceMethodResolver = sokletConfig.getResourceMethodResolver();
561                ResponseMarshaler responseMarshaler = sokletConfig.getResponseMarshaler();
562                ResponseFinalizationToken postProcessingToken = newPostProcessingToken();
563                LifecycleObserver lifecycleObserver = sokletConfig.getAggregateLifecycleObserver();
564                RequestInterceptor requestInterceptor = sokletConfig.getRequestInterceptor();
565                MetricsCollector metricsCollector = sokletConfig.getMetricsCollector();
566
567                // Holders to permit mutable effectively-final variables
568                AtomicReference<@Nullable MarshaledResponse> marshaledResponseHolder = new AtomicReference<>();
569                AtomicReference<@Nullable Throwable> resourceMethodResolutionExceptionHolder = new AtomicReference<>();
570                AtomicReference<@NonNull Request> requestHolder = new AtomicReference<>(request);
571                // The handler's effective request may be replaced by either interceptor hook. Paired
572                // lifecycle/metrics callbacks retain the original per-dispatch identity, as do HTTP
573                // stream handles, so observers never have to correlate caller-controlled request IDs.
574                AtomicReference<@Nullable ResourceMethod> resourceMethodHolder = new AtomicReference<>();
575                AtomicReference<@Nullable HttpRequestResult> requestResultHolder = new AtomicReference<>();
576                AtomicReference<@Nullable Throwable> requestHandlingFailureHolder = new AtomicReference<>();
577                Consumer<MarshaledResponse> captureHeadRepresentation = representation -> {
578                        if (serverType != ServerType.HTTP || requestHolder.get().getHttpMethod() != HttpMethod.HEAD)
579                                return;
580                        MarshaledResponseBody compressionBody = representation.getBody()
581                                        .filter(body -> body instanceof MarshaledResponseBody.Bytes || body instanceof MarshaledResponseBody.ByteBuffer)
582                                        .orElse(null);
583                        Long bodyLength = representation.getBody().map(MarshaledResponseBody::getLength).orElse(null);
584                        requestResultHolder.updateAndGet(result -> result == null
585                                        ? HttpRequestResult.withMarshaledResponse(representation).resourceMethod(resourceMethodHolder.get())
586                                                        .headResponseCompressionBody(compressionBody).headResponseBodyLength(bodyLength).build()
587                                        : result.copy().marshaledResponse(representation).headResponseCompressionBody(compressionBody)
588                                                        .headResponseBodyLength(bodyLength).finish());
589                };
590
591                // Holders to permit mutable effectively-final state tracking
592                AtomicBoolean willStartResponseWritingCompleted = new AtomicBoolean(false);
593                AtomicBoolean didFinishResponseWritingCompleted = new AtomicBoolean(false);
594                AtomicBoolean didFinishRequestHandlingCompleted = new AtomicBoolean(false);
595                AtomicBoolean didInvokeWrapRequestConsumer = new AtomicBoolean(false);
596                AtomicBoolean wrapRequestScopeOpen = new AtomicBoolean(true);
597                Thread wrapRequestThread = Thread.currentThread();
598
599                List<Throwable> throwables = new ArrayList<>(10);
600
601                // A built-in transport can replace the logical response before commitment.
602                // Keep write/finish observation aligned with that finite wire response.
603                HttpResponseStreamObservation streamObservation = new HttpResponseStreamObservation(request, processingStartedNanos);
604                HttpTransportResponseFeedback feedback = HttpTransportResponseFeedback.forConsumer(requestResultConsumer);
605                if (feedback != null)
606                        feedback.claimLifecycleConsumer();
607                Consumer<HttpRequestResult> transportResultConsumer = requestResult -> {
608                        if (serverType == ServerType.SSE)
609                                requestResult = requestResult.copy().effectiveSseRequest(requestHolder.get()).finish();
610                        if (serverType == ServerType.HTTP)
611                                requestResult = requestResult.copy().responseStreamObservation(streamObservation).finish();
612                        Throwable requestHandlingFailure = requestHandlingFailureHolder.get();
613                        if (serverType == ServerType.SSE && requestHandlingFailure != null)
614                                requestResult = requestResult.copy().sseHandshakeResult(null)
615                                                .requestHandlingFailure(requestHandlingFailure).finish();
616                        else if (serverType == ServerType.SSE
617                                        && requestResult.getSseHandshakeResult().orElse(null) instanceof SseHandshakeResult.Accepted
618                                        && !isCompatibleAcceptedSseResponse(requestResult.getMarshaledResponse()))
619                                // An interceptor can replace an accepted handshake with a finite denial.
620                                // The final response owns that decision in both live and simulated transports.
621                                requestResult = requestResult.copy().sseHandshakeResult(null).finish();
622                        HttpTransportResponseOutcome compatibilityOutcome = HttpTransportResponseOutcome.accepted();
623                        try {
624                                requestResultConsumer.accept(requestResult);
625                        } catch (HttpTransportResponseFeedback.UnclaimedReplacement replacement) {
626                                compatibilityOutcome = replacement.getOutcome();
627                        } finally {
628                                // Consume feedback even when a decorating callback fails after delivery.
629                                HttpTransportResponseOutcome outcome = feedback == null ? compatibilityOutcome : feedback.finish();
630                                if (outcome instanceof HttpTransportResponseOutcome.Accepted)
631                                        outcome = compatibilityOutcome;
632                                switch (outcome) {
633                                        case HttpTransportResponseOutcome.Accepted ignored -> {}
634                                        case HttpTransportResponseOutcome.NotWritten notWritten -> notWritten.rethrowWriteFailure();
635                                        case HttpTransportResponseOutcome.Replaced replacement -> {
636                                                marshaledResponseHolder.set(replacement.getMarshaledResponse());
637                                                requestResultHolder.set(requestResult.copy().marshaledResponse(replacement.getMarshaledResponse())
638                                                                .headResponseCompressionBody(null).headResponseBodyLength(null).finish());
639                                                replacement.getPreparationFailure().ifPresent(throwables::add);
640                                                replacement.rethrowWriteFailure();
641                                        }
642                                }
643                        }
644                };
645
646                Consumer<LogEvent> safelyLog = (logEvent -> {
647                        try {
648                                lifecycleObserver.didReceiveLogEvent(logEvent);
649                        } catch (Throwable throwable) {
650                                LifecycleObserverLogFallback.report(throwable);
651                                throwables.add(throwable);
652                        }
653                });
654
655                BiConsumer<String, Consumer<MetricsCollector>> safelyCollectMetrics = (message, metricsInvocation) -> {
656                        if (metricsCollector == null)
657                                return;
658
659                        try {
660                                metricsInvocation.accept(metricsCollector);
661                        } catch (Throwable throwable) {
662                                safelyLog.accept(LogEvent.with(LogEventType.METRICS_COLLECTOR_FAILED, message)
663                                                .throwable(throwable)
664                                                .request(requestHolder.get())
665                                                .resourceMethod(resourceMethodHolder.get())
666                                                .marshaledResponse(marshaledResponseHolder.get())
667                                                .build());
668                        }
669                };
670
671                AtomicBoolean didStartRequestHandling = new AtomicBoolean();
672                Runnable startRequestHandling = () -> {
673                        if (!didStartRequestHandling.compareAndSet(false, true))
674                                return;
675
676                        try {
677                                lifecycleObserver.didStartRequestHandling(serverType, request, resourceMethodHolder.get());
678                        } catch (Throwable t) {
679                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_START_REQUEST_HANDLING_FAILED,
680                                                                format("An exception occurred while invoking %s::didStartRequestHandling",
681                                                                                LifecycleObserver.class.getSimpleName()))
682                                                .throwable(t)
683                                                .request(requestHolder.get())
684                                                .resourceMethod(resourceMethodHolder.get())
685                                                .build());
686
687                                throwables.add(t);
688                        }
689
690                        safelyCollectMetrics.accept(
691                                        format("An exception occurred while invoking %s::didStartRequestHandling", MetricsCollector.class.getSimpleName()),
692                                        (metricsInvocation) -> metricsInvocation.didStartRequestHandling(serverType, request, resourceMethodHolder.get()));
693                };
694
695                requestHolder.set(request);
696
697                try {
698                        try {
699                        requestInterceptor.wrapRequest(serverType, request, (wrappedRequest) -> {
700                                if (!wrapRequestScopeOpen.get() || !sameInstance(Thread.currentThread(), wrapRequestThread))
701                                        throw new IllegalStateException("RequestInterceptor::wrapRequest must call requestProcessor synchronously before returning");
702                                if (!didInvokeWrapRequestConsumer.compareAndSet(false, true))
703                                        throw new IllegalStateException("RequestInterceptor::wrapRequest must call requestProcessor exactly once");
704                                requestHolder.set(wrappedRequest);
705
706                                try {
707                                        // Resolve after wrapping so path/method rewrites affect routing.
708                                        ResourceMethod resolvedResourceMethod =
709                                                        resolveResourceMethodForRequest(requireNonNull(requestHolder.get()), resourceMethodResolver, serverType);
710                                        resourceMethodHolder.set(resolvedResourceMethod);
711                                        resourceMethodResolutionExceptionHolder.set(null);
712                                } catch (Throwable t) {
713                                        safelyLog.accept(LogEvent.with(LogEventType.RESOURCE_METHOD_RESOLUTION_FAILED, "Unable to resolve Resource Method")
714                                                        .throwable(t)
715                                                        .request(requestHolder.get())
716                                                        .build());
717
718                                        // If an exception occurs here, keep track of it - we will surface them after letting LifecycleObserver
719                                        // see that a request has come in.
720                                        throwables.add(t);
721                                        resourceMethodResolutionExceptionHolder.set(t);
722                                        resourceMethodHolder.set(null);
723                                }
724
725                                startRequestHandling.run();
726
727                                // Retain generated file bodies until the interceptor chooses its final response.
728                                // The list allocates storage only for responses that actually own a channel.
729                                List<MarshaledResponse> pendingResponseBodies = new ArrayList<>(0);
730                                try {
731                                        AtomicBoolean didInvokeMarshaledResponseConsumer = new AtomicBoolean(false);
732
733                                        requestInterceptor.interceptRequest(serverType, requestHolder.get(), resourceMethodHolder.get(), (interceptorRequest) -> {
734                                                requestHolder.set(interceptorRequest);
735
736                                                try {
737                                                        if (resourceMethodResolutionExceptionHolder.get() != null)
738                                                                throw resourceMethodResolutionExceptionHolder.get();
739
740                                                        HttpRequestResult requestResult = toHttpRequestResult(requestHolder.get(), resourceMethodHolder.get(), serverType);
741                                                        requestResultHolder.set(requestResult);
742
743                                                        // The interceptor chooses the response before CORS, postprocessing and HEAD projection.
744                                                        MarshaledResponse updatedMarshaledResponse = requestResult.getMarshaledResponse();
745                                                        return ResponseBodyCleanup.track(pendingResponseBodies, updatedMarshaledResponse);
746                                                        } catch (Throwable t) {
747                                                                requestHandlingFailureHolder.compareAndSet(null, t);
748                                                                requestResultHolder.updateAndGet(result -> result == null ? null
749                                                                                : result.copy().headResponseCompressionBody(null).headResponseBodyLength(null).finish());
750                                                                if (!sameInstance(t, resourceMethodResolutionExceptionHolder.get())) {
751                                                                        throwables.add(t);
752
753                                                                safelyLog.accept(LogEvent.with(LogEventType.REQUEST_PROCESSING_FAILED,
754                                                                                                "An exception occurred while processing request")
755                                                                                .throwable(t)
756                                                                                .request(requestHolder.get())
757                                                                                .resourceMethod(resourceMethodHolder.get())
758                                                                                .build());
759                                                        }
760
761                                                        // Unhappy path.  Try to use configuration's exception response marshaler...
762                                                        try {
763                                                                MarshaledResponse marshaledResponse = responseMarshaler.forThrowable(requestHolder.get(), t, resourceMethodHolder.get());
764                                                                marshaledResponseHolder.set(marshaledResponse);
765
766                                                                return ResponseBodyCleanup.track(pendingResponseBodies, marshaledResponse);
767                                                        } catch (Throwable t2) {
768                                                                throwables.add(t2);
769
770                                                                safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
771                                                                                                "An exception occurred while trying to write an exception response")
772                                                                                .throwable(t2)
773                                                                                .request(requestHolder.get())
774                                                                                .resourceMethod(resourceMethodHolder.get())
775                                                                                .build());
776
777                                                                // The configuration's exception response marshaler failed - provide a failsafe response to recover
778                                                                return ResponseBodyCleanup.track(pendingResponseBodies, provideFailsafeMarshaledResponse(requestHolder.get(), t2));
779                                                        }
780                                                }
781                                        }, (interceptorMarshaledResponse) -> {
782                                                requireNonNull(interceptorMarshaledResponse);
783                                                ResponseBodyCleanup.track(pendingResponseBodies, interceptorMarshaledResponse);
784                                                enforceMarshaledResponseStatusCode(interceptorMarshaledResponse, false);
785                                                didInvokeMarshaledResponseConsumer.set(true);
786                                                HttpRequestResult requestResult = requestResultHolder.get();
787                                                if (serverType == ServerType.SSE && requestResult != null
788                                                                && requestResult.getSseHandshakeResult().orElse(null) instanceof SseHandshakeResult.Accepted
789                                                                && !isCompatibleAcceptedSseResponse(interceptorMarshaledResponse)) {
790                                                        requestResult = requestResult.copy().sseHandshakeResult(null).finish();
791                                                        requestResultHolder.set(requestResult);
792                                                }
793                                                if (requestResult != null && !requestResult.getMarshaledResponse().getStatusCode()
794                                                                .equals(interceptorMarshaledResponse.getStatusCode()))
795                                                        requestResultHolder.set(requestResult.copy().headResponseCompressionBody(null).headResponseBodyLength(null).finish());
796                                                marshaledResponseHolder.set(interceptorMarshaledResponse);
797                                        });
798
799                                        if (!didInvokeMarshaledResponseConsumer.get()) {
800                                                requestResultHolder.set(null);
801                                                throw new IllegalStateException(format("%s::interceptRequest must call responseWriter", RequestInterceptor.class.getSimpleName()));
802                                        }
803                                } catch (Throwable t) {
804                                        requestHandlingFailureHolder.compareAndSet(null, t);
805                                        throwables.add(t);
806                                        requestResultHolder.updateAndGet(result -> result == null ? null
807                                                        : result.copy().headResponseCompressionBody(null).headResponseBodyLength(null).finish());
808
809                                        try {
810                                                // In the event that an error occurs during processing of a RequestInterceptor method, for example
811                                                safelyLog.accept(LogEvent.with(LogEventType.REQUEST_INTERCEPTOR_INTERCEPT_REQUEST_FAILED,
812                                                                                format("An exception occurred while invoking %s::interceptRequest", RequestInterceptor.class.getSimpleName()))
813                                                                .throwable(t)
814                                                                .request(requestHolder.get())
815                                                                .resourceMethod(resourceMethodHolder.get())
816                                                                .build());
817
818                                                MarshaledResponse marshaledResponse = responseMarshaler.forThrowable(requestHolder.get(), t, resourceMethodHolder.get());
819                                                marshaledResponseHolder.set(marshaledResponse);
820                                        } catch (Throwable t2) {
821                                                throwables.add(t2);
822
823                                                safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
824                                                                                format("An exception occurred while invoking %s::forThrowable when trying to write an exception response", ResponseMarshaler.class.getSimpleName()))
825                                                                .throwable(t2)
826                                                                .request(requestHolder.get())
827                                                                .resourceMethod(resourceMethodHolder.get())
828                                                                .build());
829
830                                                marshaledResponseHolder.set(provideFailsafeMarshaledResponse(requestHolder.get(), t2));
831                                        }
832                                } finally {
833                                        // A broken exception marshaler must not turn missing response selection
834                                        // into a transport write failure or expose null responses to observers.
835                                        if (marshaledResponseHolder.get() == null) {
836                                                Throwable failure = requestHandlingFailureHolder.get();
837                                                if (failure == null)
838                                                        failure = new IllegalStateException("Request handling did not select a response");
839                                                marshaledResponseHolder.set(provideFailsafeMarshaledResponse(
840                                                                requestHolder.get() == null ? request : requestHolder.get(), failure));
841                                        }
842                                        MarshaledResponse selectedResponse = marshaledResponseHolder.get();
843                                        if (selectedResponse != null && requiresPostProcessing(selectedResponse, postProcessingToken)) {
844                                                try {
845                                                        HttpRequestResult selectedResult = requestResultHolder.get();
846                                                        boolean acceptedSse = serverType == ServerType.SSE && selectedResult != null
847                                                                        && selectedResult.getSseHandshakeResult().orElse(null) instanceof SseHandshakeResult.Accepted;
848                                                        MarshaledResponse finalized = ResponseBodyCleanup.transform(selectedResponse,
849                                                                        response -> applyCommonPropertiesToMarshaledResponse(requestHolder.get(), response, acceptedSse, captureHeadRepresentation, postProcessingToken));
850                                                        try {
851                                                                if (acceptedSse) isCompatibleAcceptedSseResponse(finalized);
852                                                        } catch (RuntimeException | Error failure) {
853                                                                ResponseBodyCleanup.discard(finalized, null, failure);
854                                                                throw failure;
855                                                        }
856                                                        marshaledResponseHolder.set(finalized);
857                                                } catch (Throwable failure) {
858                                                        throwables.add(failure);
859                                                        requestHandlingFailureHolder.compareAndSet(null, failure);
860                                                        requestResultHolder.updateAndGet(result -> result == null ? null
861                                                                        : result.copy().headResponseCompressionBody(null).headResponseBodyLength(null).finish());
862                                                        safelyLog.accept(LogEvent.with(LogEventType.REQUEST_PROCESSING_FAILED,
863                                                                        "An exception occurred while finalizing response").throwable(failure)
864                                                                        .request(requestHolder.get()).resourceMethod(resourceMethodHolder.get()).build());
865                                                        try {
866                                                                MarshaledResponse recovery = responseMarshaler.forThrowable(requestHolder.get(), failure, resourceMethodHolder.get());
867                                                                recovery = ResponseBodyCleanup.transform(recovery, response -> applyCommonPropertiesToMarshaledResponse(
868                                                                                requestHolder.get(), response, false, ignored -> {}, postProcessingToken, false));
869                                                                marshaledResponseHolder.set(recovery);
870                                                        } catch (Throwable recoveryFailure) {
871                                                                throwables.add(recoveryFailure);
872                                                                safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
873                                                                                "Unable to finalize exception response").throwable(recoveryFailure)
874                                                                                .request(requestHolder.get()).resourceMethod(resourceMethodHolder.get()).build());
875                                                                marshaledResponseHolder.set(provideFailsafeMarshaledResponse(requestHolder.get(), recoveryFailure));
876                                                        }
877                                                }
878                                        }
879                                        for (MarshaledResponse pendingResponse : pendingResponseBodies) {
880                                                try {
881                                                        MarshaledResponse selected = marshaledResponseHolder.get();
882                                                        if (selected == null) pendingResponse.close();
883                                                        else marshaledResponseHolder.set(ResponseBodyCleanup.retain(pendingResponse, selected));
884                                                } catch (Throwable cleanupFailure) {
885                                                        throwables.add(cleanupFailure);
886                                                        safelyLog.accept(LogEvent.with(LogEventType.REQUEST_PROCESSING_FAILED,
887                                                                        "Unable to release an abandoned response body")
888                                                                        .throwable(cleanupFailure).request(requestHolder.get())
889                                                                        .resourceMethod(resourceMethodHolder.get()).build());
890                                                }
891                                        }
892                                        pendingResponseBodies.clear();
893                                        try {
894                                                try {
895                                                        lifecycleObserver.willWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get());
896                                                } catch (Throwable t) {
897                                                        throwables.add(t);
898                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_WILL_WRITE_RESPONSE_FAILED,
899                                                                        format("An exception occurred while invoking %s::willWriteResponse", LifecycleObserver.class.getSimpleName()))
900                                                                        .throwable(t).request(requestHolder.get()).resourceMethod(resourceMethodHolder.get())
901                                                                        .marshaledResponse(marshaledResponseHolder.get()).build());
902                                                } finally {
903                                                        willStartResponseWritingCompleted.set(true);
904                                                }
905
906                                                safelyCollectMetrics.accept(
907                                                                format("An exception occurred while invoking %s::willWriteResponse", MetricsCollector.class.getSimpleName()),
908                                                                (metricsInvocation) -> metricsInvocation.willWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get()));
909
910                                                Instant responseWriteStarted = Instant.now();
911
912                                                try {
913                                                        HttpRequestResult requestResult = requestResultHolder.get();
914
915                                                        if (requestResult != null)
916                                                                transportResultConsumer.accept(requestResult.copy()
917                                                                                .marshaledResponse(requireNonNull(marshaledResponseHolder.get())).finish());
918                                                        else
919                                                                transportResultConsumer.accept(HttpRequestResult.withMarshaledResponse(marshaledResponseHolder.get())
920                                                                                .resourceMethod(resourceMethodHolder.get())
921                                                                                .build());
922
923                                                        Instant responseWriteFinished = Instant.now();
924                                                        Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
925
926                                                        try {
927                                                                lifecycleObserver.didWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration);
928                                                        } catch (Throwable t) {
929                                                                throwables.add(t);
930
931                                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_WRITE_RESPONSE_FAILED,
932                                                                                                format("An exception occurred while invoking %s::didWriteResponse",
933                                                                                                                LifecycleObserver.class.getSimpleName()))
934                                                                                .throwable(t)
935                                                                                .request(requestHolder.get())
936                                                                                .resourceMethod(resourceMethodHolder.get())
937                                                                                .marshaledResponse(marshaledResponseHolder.get())
938                                                                                .build());
939                                                        } finally {
940                                                                didFinishResponseWritingCompleted.set(true);
941                                                        }
942
943                                                        safelyCollectMetrics.accept(
944                                                                        format("An exception occurred while invoking %s::didWriteResponse", MetricsCollector.class.getSimpleName()),
945                                                                        (metricsInvocation) -> metricsInvocation.didWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(),
946                                                                                        marshaledResponseHolder.get(), responseWriteDuration));
947                                                } catch (Throwable t) {
948                                                        didFinishResponseWritingCompleted.set(true);
949                                                        throwables.add(t);
950
951                                                        Instant responseWriteFinished = Instant.now();
952                                                        Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
953
954                                                        try {
955                                                                lifecycleObserver.didFailToWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration, t);
956                                                        } catch (Throwable t2) {
957                                                                throwables.add(t2);
958
959                                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_WRITE_RESPONSE_FAILED,
960                                                                                                format("An exception occurred while invoking %s::didFailToWriteResponse",
961                                                                                                                LifecycleObserver.class.getSimpleName()))
962                                                                                .throwable(t2)
963                                                                                .request(requestHolder.get())
964                                                                                .resourceMethod(resourceMethodHolder.get())
965                                                                                .marshaledResponse(marshaledResponseHolder.get())
966                                                                                .build());
967                                                        }
968
969                                                        safelyCollectMetrics.accept(
970                                                                        format("An exception occurred while invoking %s::didFailToWriteResponse", MetricsCollector.class.getSimpleName()),
971                                                                        (metricsInvocation) -> metricsInvocation.didFailToWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(),
972                                                                                        marshaledResponseHolder.get(), responseWriteDuration, t));
973                                                }
974                                        } finally {
975                                                Duration processingDuration = Duration.ofNanos(Math.max(0L,
976                                                                System.nanoTime() - processingStartedNanos));
977
978                                                safelyCollectMetrics.accept(
979                                                                format("An exception occurred while invoking %s::didFinishRequestHandling", MetricsCollector.class.getSimpleName()),
980                                                                (metricsInvocation) -> metricsInvocation.didFinishRequestHandling(serverType, request, resourceMethodHolder.get(), marshaledResponseHolder.get(), processingDuration, Collections.unmodifiableList(throwables)));
981
982                                        MarshaledResponse finishedResponse = marshaledResponseHolder.get();
983                                        streamObservation.completeHandling(serverType == ServerType.HTTP && finishedResponse != null && finishedResponse.isStreaming());
984
985                                                try {
986                                                        lifecycleObserver.didFinishRequestHandling(serverType, request, resourceMethodHolder.get(), marshaledResponseHolder.get(), processingDuration, Collections.unmodifiableList(throwables));
987                                                } catch (Throwable t) {
988                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_FINISH_REQUEST_HANDLING_FAILED,
989                                                                                        format("An exception occurred while invoking %s::didFinishRequestHandling",
990                                                                                                        LifecycleObserver.class.getSimpleName()))
991                                                                        .throwable(t)
992                                                                        .request(requestHolder.get())
993                                                                        .resourceMethod(resourceMethodHolder.get())
994                                                                        .marshaledResponse(marshaledResponseHolder.get())
995                                                                        .build());
996                                                } finally {
997                                                        didFinishRequestHandlingCompleted.set(true);
998                                                }
999                                        }
1000                                }
1001                        });
1002                        } finally {
1003                                wrapRequestScopeOpen.set(false);
1004                        }
1005
1006                        if (!didInvokeWrapRequestConsumer.get())
1007                                throw new IllegalStateException(format("%s::wrapRequest must call requestProcessor", RequestInterceptor.class.getSimpleName()));
1008                } catch (Throwable t) {
1009                        requestHandlingFailureHolder.compareAndSet(null, t);
1010                        if (!didFinishRequestHandlingCompleted.get())
1011                                throwables.add(t);
1012                        // If an error occurred during request wrapping, it's possible a response was never written/communicated back to LifecycleObserver.
1013                        // Detect that here and inform LifecycleObserver accordingly.
1014                        safelyLog.accept(LogEvent.with(LogEventType.REQUEST_INTERCEPTOR_WRAP_REQUEST_FAILED,
1015                                                        format("An exception occurred while invoking %s::wrapRequest",
1016                                                                        RequestInterceptor.class.getSimpleName()))
1017                                        .throwable(t)
1018                                        .request(requestHolder.get())
1019                                        .resourceMethod(resourceMethodHolder.get())
1020                                        .marshaledResponse(marshaledResponseHolder.get())
1021                                        .build());
1022
1023                        // Wrapping can fail before dispatch; keep start/finish callbacks paired for observers and metrics.
1024                        startRequestHandling.run();
1025
1026                        requestResultHolder.updateAndGet(result -> result == null ? null
1027                                        : result.copy().headResponseCompressionBody(null).headResponseBodyLength(null).finish());
1028
1029                        // If we don't have a response, let the marshaler try to make one for the exception.
1030                        // If that fails, use the failsafe.
1031                        if (marshaledResponseHolder.get() == null) {
1032                                try {
1033                                        MarshaledResponse marshaledResponse = responseMarshaler.forThrowable(requestHolder.get(), t, resourceMethodHolder.get());
1034                                        marshaledResponse = ResponseBodyCleanup.transform(marshaledResponse,
1035                                                        response -> applyCommonPropertiesToMarshaledResponse(requestHolder.get(), response, false, ignored -> {}, postProcessingToken, false));
1036                                        marshaledResponseHolder.set(marshaledResponse);
1037                                } catch (Throwable t2) {
1038                                        throwables.add(t2);
1039
1040                                        safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
1041                                                                        format("An exception occurred during request wrapping while invoking %s::forThrowable",
1042                                                                                        ResponseMarshaler.class.getSimpleName()))
1043                                                        .throwable(t2)
1044                                                        .request(requestHolder.get())
1045                                                        .resourceMethod(resourceMethodHolder.get())
1046                                                        .marshaledResponse(marshaledResponseHolder.get())
1047                                                        .build());
1048
1049                                        marshaledResponseHolder.set(provideFailsafeMarshaledResponse(requestHolder.get(), t));
1050                                }
1051                        }
1052
1053                        if (!willStartResponseWritingCompleted.get()) {
1054                                try {
1055                                        lifecycleObserver.willWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get());
1056                                } catch (Throwable t2) {
1057                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_WILL_WRITE_RESPONSE_FAILED,
1058                                                                        format("An exception occurred while invoking %s::willWriteResponse",
1059                                                                                        LifecycleObserver.class.getSimpleName()))
1060                                                        .throwable(t2)
1061                                                        .request(requestHolder.get())
1062                                                        .resourceMethod(resourceMethodHolder.get())
1063                                                        .marshaledResponse(marshaledResponseHolder.get())
1064                                                        .build());
1065                                }
1066
1067                                safelyCollectMetrics.accept(
1068                                                format("An exception occurred while invoking %s::willWriteResponse", MetricsCollector.class.getSimpleName()),
1069                                                (metricsInvocation) -> metricsInvocation.willWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get()));
1070                        }
1071
1072                        try {
1073                                Instant responseWriteStarted = Instant.now();
1074
1075                                if (!didFinishResponseWritingCompleted.get()) {
1076                                        try {
1077                                                HttpRequestResult requestResult = requestResultHolder.get();
1078
1079                                                if (requestResult != null)
1080                                                        transportResultConsumer.accept(requestResult.copy()
1081                                                                        .marshaledResponse(requireNonNull(marshaledResponseHolder.get())).finish());
1082                                                else
1083                                                        transportResultConsumer.accept(HttpRequestResult.withMarshaledResponse(marshaledResponseHolder.get())
1084                                                                        .resourceMethod(resourceMethodHolder.get())
1085                                                                        .build());
1086
1087                                                Instant responseWriteFinished = Instant.now();
1088                                                Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
1089
1090                                                try {
1091                                                        lifecycleObserver.didWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration);
1092                                                } catch (Throwable t2) {
1093                                                        throwables.add(t2);
1094
1095                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_WRITE_RESPONSE_FAILED,
1096                                                                                        format("An exception occurred while invoking %s::didWriteResponse",
1097                                                                                                        LifecycleObserver.class.getSimpleName()))
1098                                                                        .throwable(t2)
1099                                                                        .request(requestHolder.get())
1100                                                                        .resourceMethod(resourceMethodHolder.get())
1101                                                                        .marshaledResponse(marshaledResponseHolder.get())
1102                                                                        .build());
1103                                                }
1104
1105                                                safelyCollectMetrics.accept(
1106                                                                format("An exception occurred while invoking %s::didWriteResponse", MetricsCollector.class.getSimpleName()),
1107                                                                (metricsInvocation) -> metricsInvocation.didWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(),
1108                                                                                marshaledResponseHolder.get(), responseWriteDuration));
1109                                        } catch (Throwable t2) {
1110                                                throwables.add(t2);
1111
1112                                                Instant responseWriteFinished = Instant.now();
1113                                                Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
1114
1115                                                try {
1116                                                        lifecycleObserver.didFailToWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration, t2);
1117                                                } catch (Throwable t3) {
1118                                                        throwables.add(t3);
1119
1120                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_WRITE_RESPONSE_FAILED,
1121                                                                                        format("An exception occurred while invoking %s::didFailToWriteResponse",
1122                                                                                                        LifecycleObserver.class.getSimpleName()))
1123                                                                        .throwable(t3)
1124                                                                        .request(requestHolder.get())
1125                                                                        .resourceMethod(resourceMethodHolder.get())
1126                                                                        .marshaledResponse(marshaledResponseHolder.get())
1127                                                                        .build());
1128                                                }
1129
1130                                                safelyCollectMetrics.accept(
1131                                                                format("An exception occurred while invoking %s::didFailToWriteResponse", MetricsCollector.class.getSimpleName()),
1132                                                                (metricsInvocation) -> metricsInvocation.didFailToWriteResponse(serverType, requestHolder.get(), resourceMethodHolder.get(),
1133                                                                                marshaledResponseHolder.get(), responseWriteDuration, t2));
1134                                        }
1135                                }
1136                        } finally {
1137                                if (!didFinishRequestHandlingCompleted.get()) {
1138                                        Duration processingDuration = Duration.ofNanos(Math.max(0L,
1139                                                        System.nanoTime() - processingStartedNanos));
1140
1141                                        safelyCollectMetrics.accept(
1142                                                        format("An exception occurred while invoking %s::didFinishRequestHandling", MetricsCollector.class.getSimpleName()),
1143                                                        (metricsInvocation) -> metricsInvocation.didFinishRequestHandling(serverType, request, resourceMethodHolder.get(), marshaledResponseHolder.get(), processingDuration, Collections.unmodifiableList(throwables)));
1144
1145                                        MarshaledResponse finishedResponse = marshaledResponseHolder.get();
1146                                        streamObservation.completeHandling(serverType == ServerType.HTTP && finishedResponse != null && finishedResponse.isStreaming());
1147
1148                                        try {
1149                                                lifecycleObserver.didFinishRequestHandling(serverType, request, resourceMethodHolder.get(), marshaledResponseHolder.get(), processingDuration, Collections.unmodifiableList(throwables));
1150                                        } catch (Throwable t2) {
1151                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_FINISH_REQUEST_HANDLING_FAILED,
1152                                                                                format("An exception occurred while invoking %s::didFinishRequestHandling",
1153                                                                                                LifecycleObserver.class.getSimpleName()))
1154                                                                .throwable(t2)
1155                                                                .request(requestHolder.get())
1156                                                                .resourceMethod(resourceMethodHolder.get())
1157                                                                .marshaledResponse(marshaledResponseHolder.get())
1158                                                                .build());
1159                                        }
1160                                }
1161                        }
1162                }
1163        }
1164
1165        @NonNull
1166        HttpRequestResult toHttpRequestResult(@NonNull Request request,
1167                                                                                                                                                                        @Nullable ResourceMethod resourceMethod,
1168                                                                                                                                                                        @NonNull ServerType serverType) throws Throwable {
1169                requireNonNull(request);
1170                requireNonNull(serverType);
1171
1172                ResourceMethodParameterProvider resourceMethodParameterProvider = getSokletConfig().getResourceMethodParameterProvider();
1173                InstanceProvider instanceProvider = getSokletConfig().getInstanceProvider();
1174                CorsAuthorizer corsAuthorizer = getSokletConfig().getCorsAuthorizer();
1175                ResourceMethodResolver resourceMethodResolver = getSokletConfig().getResourceMethodResolver();
1176                ResponseMarshaler responseMarshaler = getSokletConfig().getResponseMarshaler();
1177                CorsPreflight corsPreflight = request.getCorsPreflight().orElse(null);
1178
1179                // Special short-circuit for big requests
1180                if (request.isContentTooLarge()) {
1181                        ResourceMethod contentTooLargeResourceMethod =
1182                                        resolveResourceMethodForRequest(request, resourceMethodResolver, serverType);
1183                        return HttpRequestResult.withMarshaledResponse(responseMarshaler.forContentTooLarge(request, contentTooLargeResourceMethod))
1184                                        .resourceMethod(resourceMethod)
1185                                        .build();
1186                }
1187
1188                // Special short-circuit for OPTIONS *
1189                if (isOptionsSplat(request.getResourcePath()))
1190                        return HttpRequestResult.withMarshaledResponse(responseMarshaler.forOptionsSplat(request)).build();
1191
1192                // CORS preflight authorization precedes explicit OPTIONS handlers too.
1193                if (resourceMethod == null || corsPreflight != null) {
1194                        // If this was an OPTIONS request, do special processing.
1195                        // If not, figure out if we should return a 404 or 405.
1196                        if (request.getHttpMethod() == HttpMethod.OPTIONS) {
1197                                // See what methods are available to us for this request's path
1198                                Map<HttpMethod, ResourceMethod> matchingResourceMethodsByHttpMethod = resolveMatchingResourceMethodsByHttpMethod(request, resourceMethodResolver, serverType);
1199
1200                                // Special handling for CORS preflight requests, if needed
1201                                if (corsPreflight != null) {
1202                                        // Let configuration function determine if we should authorize this request.
1203                                        // Discard any OPTIONS references - see https://stackoverflow.com/a/68529748
1204                                        Map<HttpMethod, ResourceMethod> nonOptionsMatchingResourceMethodsByHttpMethod = matchingResourceMethodsByHttpMethod.entrySet().stream()
1205                                                        .filter(entry -> entry.getKey() != HttpMethod.OPTIONS)
1206                                                        .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
1207
1208                                        // Validate client-supplied reflection candidates before application
1209                                        // authorization, including custom authorizers that reflect them verbatim.
1210                                        CorsPreflightResponse corsPreflightResponse = corsPreflight.isSafeToReflect()
1211                                                        ? corsAuthorizer.authorizePreflight(request, corsPreflight, nonOptionsMatchingResourceMethodsByHttpMethod).orElse(null)
1212                                                        : null;
1213
1214                                        // Allow or reject CORS depending on what the function said to do
1215                                        if (corsPreflightResponse != null) {
1216                                                // Allow
1217                                                MarshaledResponse marshaledResponse = responseMarshaler.forCorsPreflightAllowed(request, corsPreflight, corsPreflightResponse);
1218
1219                                                return HttpRequestResult.withMarshaledResponse(marshaledResponse)
1220                                                                .corsPreflightResponse(corsPreflightResponse)
1221                                                                .build();
1222                                        }
1223
1224                                        // Reject
1225                                        return HttpRequestResult.withMarshaledResponse(responseMarshaler.forCorsPreflightRejected(request, corsPreflight))
1226                                                        .build();
1227                                } else {
1228                                        // Just a normal OPTIONS response (non-CORS-preflight).
1229                                        if (matchingResourceMethodsByHttpMethod.isEmpty())
1230                                                return HttpRequestResult.withMarshaledResponse(responseMarshaler.forNotFound(request)).build();
1231
1232                                        // If there's a matching OPTIONS resource method for this OPTIONS request, then invoke it.
1233                                        ResourceMethod optionsResourceMethod = matchingResourceMethodsByHttpMethod.get(HttpMethod.OPTIONS);
1234
1235                                        if (optionsResourceMethod != null) {
1236                                                resourceMethod = optionsResourceMethod;
1237                                        } else {
1238                                                Set<HttpMethod> allowedHttpMethods = allowedHttpMethodsForResponse(matchingResourceMethodsByHttpMethod, true, serverType);
1239
1240                                                return HttpRequestResult.withMarshaledResponse(responseMarshaler.forOptions(request, allowedHttpMethods))
1241                                                                .build();
1242                                        }
1243                                }
1244                        } else if (request.getHttpMethod() == HttpMethod.HEAD && serverType == ServerType.HTTP) {
1245                                // If there's a matching GET resource method for this HEAD request, then invoke it
1246                                Request headGetRequest = request.copy().httpMethod(HttpMethod.GET).finish();
1247                                ResourceMethod headGetResourceMethod =
1248                                                validateResolvedResourceMethod(resourceMethodResolver
1249                                                                .resourceMethodForRequest(headGetRequest, serverType)
1250                                                                .orElse(null));
1251
1252                                if (headGetResourceMethod != null)
1253                                        resourceMethod = headGetResourceMethod;
1254                                else {
1255                                        Map<HttpMethod, ResourceMethod> otherMethods = resolveMatchingResourceMethodsByHttpMethod(
1256                                                        request, resourceMethodResolver, serverType);
1257                                        boolean resourceExists = otherMethods.keySet().stream().anyMatch(method -> method != HttpMethod.OPTIONS);
1258                                        return HttpRequestResult.withMarshaledResponse(resourceExists
1259                                                        ? responseMarshaler.forMethodNotAllowed(request, allowedHttpMethodsForResponse(otherMethods, true, serverType))
1260                                                        : responseMarshaler.forNotFound(request)).build();
1261                                }
1262                        } else {
1263                                // Not an OPTIONS request, so it's possible we have a 405. See if other HTTP methods match...
1264                                Map<HttpMethod, ResourceMethod> otherMatchingResourceMethodsByHttpMethod = resolveMatchingResourceMethodsByHttpMethod(request, resourceMethodResolver, serverType);
1265
1266                                Set<HttpMethod> matchingNonOptionsHttpMethods = otherMatchingResourceMethodsByHttpMethod.keySet().stream()
1267                                                .filter(httpMethod -> httpMethod != HttpMethod.OPTIONS)
1268                                                .collect(Collectors.toSet());
1269
1270                                if (matchingNonOptionsHttpMethods.size() > 0) {
1271                                        // ...if some do, it's a 405
1272                                        Set<HttpMethod> allowedHttpMethods = allowedHttpMethodsForResponse(otherMatchingResourceMethodsByHttpMethod, true, serverType);
1273                                        return HttpRequestResult.withMarshaledResponse(responseMarshaler.forMethodNotAllowed(request, allowedHttpMethods))
1274                                                        .build();
1275                                } else {
1276                                        // no matching resource method found, it's a 404
1277                                        return HttpRequestResult.withMarshaledResponse(responseMarshaler.forNotFound(request))
1278                                                        .build();
1279                                }
1280                        }
1281                }
1282
1283                // Found a resource method - happy path.
1284                // 1. Get an instance of the resource class
1285                // 2. Get values to pass to the resource method on the resource class
1286                // 3. Invoke the resource method and use its return value to drive a response
1287                SokletFrameworkSetup.validateConfiguredResourceMethod(getSokletConfig(), resourceMethod);
1288                Class<?> resourceClass = resourceMethod.getMethod().getDeclaringClass();
1289                Object resourceClassInstance;
1290
1291                try {
1292                        resourceClassInstance = instanceProvider.provide(resourceClass);
1293                } catch (Exception e) {
1294                        throw new IllegalArgumentException(format("Unable to acquire an instance of %s", resourceClass.getName()), e);
1295                }
1296
1297                List<Object> parameterValues = resourceMethodParameterProvider.parameterValuesForResourceMethod(request, resourceMethod);
1298
1299                Object responseObject;
1300
1301                try {
1302                        responseObject = resourceMethod.getMethod().invoke(resourceClassInstance, parameterValues.toArray());
1303                } catch (InvocationTargetException e) {
1304                        if (e.getTargetException() != null)
1305                                throw e.getTargetException();
1306
1307                        throw e;
1308                }
1309
1310                // Unwrap the Optional<T>, if one exists.  We do not recurse deeper than one level
1311                if (responseObject instanceof Optional<?>)
1312                        responseObject = ((Optional<?>) responseObject).orElse(null);
1313
1314                Response response;
1315                SseHandshakeResult sseHandshakeResult = null;
1316
1317                // If null/void return, it's a 204
1318                // If it's a MarshaledResponse object, no marshaling + return it immediately - caller knows exactly what it wants to write.
1319                // If it's a Response object, use as is.
1320                // If it's a non-Response type of object, assume it's the response body and wrap in a Response.
1321                if (responseObject == null) {
1322                        response = Response.withStatusCode(204).build();
1323                } else if (responseObject instanceof MarshaledResponse) {
1324                        MarshaledResponse marshaledResponse = normalizeEmptyResetContent((MarshaledResponse) responseObject);
1325                        enforceMarshaledResponseStatusCode(marshaledResponse, true);
1326
1327                        return HttpRequestResult.withMarshaledResponse(marshaledResponse)
1328                                        .resourceMethod(resourceMethod)
1329                                        .build();
1330                } else if (responseObject instanceof Response) {
1331                        response = (Response) responseObject;
1332                } else if (responseObject instanceof SseHandshakeResult.Accepted accepted) { // SSE "accepted" handshake
1333                        return HttpRequestResult.withMarshaledResponse(toMarshaledResponse(accepted))
1334                                        .resourceMethod(resourceMethod)
1335                                        .sseHandshakeResult(accepted)
1336                                        .build();
1337                } else if (responseObject instanceof SseHandshakeResult.Rejected rejected) { // SSE "rejected" handshake
1338                        response = rejected.getResponse();
1339                        sseHandshakeResult = rejected;
1340                } else {
1341                        response = Response.withStatusCode(200).body(responseObject).build();
1342                }
1343
1344                if (response.getStatusCode() == 205) {
1345                        Object body = response.getBody().orElse(null);
1346                        if ((body instanceof String text && text.isEmpty()) || (body instanceof byte[] bytes && bytes.length == 0))
1347                                response = response.copy().body(null).finish();
1348                }
1349                enforceBodylessStatusCode(response.getStatusCode(), response.getBody().isPresent());
1350
1351                MarshaledResponse marshaledResponse = normalizeEmptyResetContent(responseMarshaler.forResourceMethod(request, response, resourceMethod));
1352
1353                enforceMarshaledResponseStatusCode(marshaledResponse, true);
1354
1355                return HttpRequestResult.withMarshaledResponse(marshaledResponse)
1356                                .response(response)
1357                                .resourceMethod(resourceMethod)
1358                                .sseHandshakeResult(sseHandshakeResult)
1359                                .build();
1360        }
1361
1362        @NonNull
1363        private MarshaledResponse toMarshaledResponse(SseHandshakeResult.@NonNull Accepted accepted) {
1364                requireNonNull(accepted);
1365
1366                Map<String, List<String>> headers = accepted.getHeaders();
1367                Set<String> forbiddenHeaderNames = Set.of("content-length", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade");
1368                for (String headerName : headers.keySet())
1369                        if (forbiddenHeaderNames.contains(headerName.toLowerCase(java.util.Locale.ENGLISH)))
1370                                throw new IllegalArgumentException("Unsupported framing header for an accepted SSE handshake");
1371                LinkedCaseInsensitiveMap<List<String>> finalHeaders = new LinkedCaseInsensitiveMap<>(DEFAULT_ACCEPTED_HANDSHAKE_HEADERS.size() + headers.size());
1372
1373                // Start with defaults
1374                for (Map.Entry<String, List<String>> e : DEFAULT_ACCEPTED_HANDSHAKE_HEADERS.entrySet())
1375                        finalHeaders.put(e.getKey(), e.getValue()); // values already unmodifiable
1376
1377                // Overlay user-supplied headers (prefer user values on key collision)
1378                for (Map.Entry<String, List<String>> e : headers.entrySet()) {
1379                        String headerName = requireNonNull(e.getKey());
1380                        if (headerName.equalsIgnoreCase("Connection")
1381                                        || headerName.equalsIgnoreCase("Keep-Alive"))
1382                                continue;
1383                        // Defensively copy so callers can't mutate after construction
1384                        List<String> values = e.getValue() == null ? List.of() : List.copyOf(e.getValue());
1385                        finalHeaders.put(headerName, values);
1386                }
1387                List<String> contentTypes = finalHeaders.getOrDefault("Content-Type", List.of());
1388                if (contentTypes == null || contentTypes.size() != 1
1389                                || !contentTypes.get(0).split(";", 2)[0].trim().equalsIgnoreCase("text/event-stream"))
1390                        throw new IllegalArgumentException("An accepted SSE handshake requires exactly one text/event-stream Content-Type");
1391
1392                return MarshaledResponse.withStatusCode(200)
1393                                .headers(finalHeaders)
1394                                .cookies(accepted.getCookies())
1395                                .build();
1396        }
1397
1398        private static boolean isCompatibleAcceptedSseResponse(@NonNull MarshaledResponse response) {
1399                if (response.getStatusCode() != 200 || response.isStreaming() || response.getBody().isPresent())
1400                        return false;
1401                List<String> contentTypes = response.getHeaders().getOrDefault("Content-Type", List.of());
1402                boolean compatible = contentTypes.size() == 1
1403                                && contentTypes.get(0).split(";", 2)[0].trim().equalsIgnoreCase("text/event-stream");
1404                if (compatible) {
1405                        Set<String> forbidden = Set.of("content-length", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade");
1406                        for (String headerName : response.getHeaders().keySet())
1407                                if (forbidden.contains(headerName.toLowerCase(java.util.Locale.ENGLISH)))
1408                                        throw new IllegalArgumentException("Unsupported framing header for an accepted SSE handshake");
1409                }
1410                return compatible;
1411        }
1412
1413        @NonNull
1414        private static MarshaledResponse normalizeEmptyResetContent(@NonNull MarshaledResponse response) {
1415                if (response.getStatusCode() == 205 && !response.isStreaming() && response.getBody().isPresent()
1416                                && response.getBodyLength() == 0L)
1417                        return response.copy().body((MarshaledResponseBody) null).finish();
1418                return response;
1419        }
1420
1421        private static void enforceBodylessStatusCode(@NonNull Integer statusCode,
1422                                                                                                                                                                                                @NonNull Boolean hasBody) {
1423                requireNonNull(statusCode);
1424                requireNonNull(hasBody);
1425                enforceFinalResponseStatusCode(statusCode);
1426
1427                if (hasBody && isBodylessStatusCode(statusCode))
1428                        throw new IllegalStateException(format("HTTP status code %d must not include a response body", statusCode));
1429        }
1430
1431        private static void enforceMarshaledResponseStatusCode(@NonNull MarshaledResponse response,
1432                        boolean resourceMethodResponse) {
1433                try {
1434                        if (resourceMethodResponse)
1435                                enforceBodylessStatusCode(response.getStatusCode(),
1436                                                response.getBody().isPresent() || response.getStreamingResponseBody().isPresent());
1437                        else
1438                                enforceFinalResponseStatusCode(response.getStatusCode());
1439                } catch (RuntimeException | Error failure) {
1440                        // This response will never reach transport cleanup. Release only an
1441                        // explicitly owned finite body and keep validation as the primary failure.
1442                        try {
1443                                response.close();
1444                        } catch (RuntimeException | Error cleanupFailure) {
1445                                if (!sameInstance(cleanupFailure, failure))
1446                                        failure.addSuppressed(cleanupFailure);
1447                        }
1448                        throw failure;
1449                }
1450        }
1451
1452        private static void enforceFinalResponseStatusCode(@NonNull Integer statusCode) {
1453                requireNonNull(statusCode);
1454                if (statusCode < 200 || statusCode > 599)
1455                        throw new IllegalStateException("Final HTTP response status code must be between 200 and 599.");
1456        }
1457
1458        private static boolean isBodylessStatusCode(@NonNull Integer statusCode) {
1459                requireNonNull(statusCode);
1460                return (statusCode >= 100 && statusCode < 200) || statusCode == 204 || statusCode == 205 || statusCode == 304;
1461        }
1462
1463        @NonNull
1464        MarshaledResponse applyHeadResponseIfApplicable(@NonNull Request request,
1465                                                                                                                                                                                                                                                @NonNull MarshaledResponse marshaledResponse) {
1466                if (request.getHttpMethod() != HttpMethod.HEAD)
1467                        return marshaledResponse;
1468
1469                return getSokletConfig().getResponseMarshaler().forHead(request, marshaledResponse);
1470        }
1471
1472        // Hat tip to Aslan Parçası and GrayStar
1473        @NonNull
1474        MarshaledResponse applyCommonPropertiesToMarshaledResponse(@NonNull Request request,
1475                                                                                                                                                                                                                                                                                         @NonNull MarshaledResponse marshaledResponse) {
1476                requireNonNull(request);
1477                requireNonNull(marshaledResponse);
1478
1479                return applyCommonPropertiesToMarshaledResponse(request, marshaledResponse, false);
1480        }
1481
1482        @NonNull
1483        MarshaledResponse applyCommonPropertiesToMarshaledResponse(@NonNull Request request,
1484                                                                                                                                                                                                                                                                                         @NonNull MarshaledResponse marshaledResponse,
1485                                                                                                                                                                                                                                                                                         @NonNull Boolean suppressContentLength) {
1486                return applyCommonPropertiesToMarshaledResponse(request, marshaledResponse, suppressContentLength, ignored -> {}, newPostProcessingToken());
1487        }
1488
1489        @NonNull
1490        private static final class ResponseFinalizationToken {
1491                private boolean postProcessingStarted;
1492        }
1493
1494        private @NonNull ResponseFinalizationToken newPostProcessingToken() {
1495                // Recovery must not repeat a processor that completed or failed before a later hook failed.
1496                return new ResponseFinalizationToken();
1497        }
1498
1499        private boolean requiresPostProcessing(@NonNull MarshaledResponse response, @Nullable ResponseFinalizationToken token) {
1500                return token != null && !response.isPostProcessedFor(token);
1501        }
1502
1503        @NonNull
1504        private MarshaledResponse applyCommonPropertiesToMarshaledResponse(@NonNull Request request,
1505                        @NonNull MarshaledResponse response, boolean suppressContentLength,
1506                        @NonNull Consumer<@NonNull MarshaledResponse> headRepresentationConsumer, @Nullable ResponseFinalizationToken postProcessingToken) {
1507                return applyCommonPropertiesToMarshaledResponse(request, response, suppressContentLength,
1508                                headRepresentationConsumer, postProcessingToken, true);
1509        }
1510
1511        @NonNull
1512        private MarshaledResponse applyCommonPropertiesToMarshaledResponse(@NonNull Request request,
1513                        @NonNull MarshaledResponse response, boolean suppressContentLength,
1514                        @NonNull Consumer<@NonNull MarshaledResponse> headRepresentationConsumer, @Nullable ResponseFinalizationToken postProcessingToken, boolean applyHead) {
1515                requireNonNull(request);
1516                requireNonNull(response);
1517                boolean head = applyHead && request.getHttpMethod() == HttpMethod.HEAD;
1518                response = normalizeBodylessMarshaledResponse(request, response);
1519                response = ResponseBodyCleanup.transform(response, current -> applyCorsResponseIfApplicable(request, current));
1520                if (requiresPostProcessing(response, postProcessingToken)) {
1521                        ResponseFinalizationToken token = requireNonNull(postProcessingToken);
1522                        if (!token.postProcessingStarted) {
1523                                token.postProcessingStarted = true;
1524                                ResponseMarshaler marshaler = getSokletConfig().getResponseMarshaler();
1525                                response = ResponseBodyCleanup.transform(response, marshaler::postProcess);
1526                        }
1527                        response = response.markPostProcessed(token);
1528                }
1529                @Nullable ResponseFinalizationToken completedPostProcessingToken =
1530                                postProcessingToken != null && response.isPostProcessedFor(postProcessingToken)
1531                                                ? postProcessingToken : null;
1532                MarshaledResponse headRepresentation = response;
1533                if (head) {
1534                        response = ResponseBodyCleanup.transform(response, current -> applyHeadResponseIfApplicable(request, current));
1535                        // Only a projection of this representation may borrow its discarded body.
1536                        // A fresh descriptor or changed representation metadata belongs to the HEAD handler.
1537                        List<String> projectedLength = response.getHeaders().get("Content-Length");
1538                        boolean preservesRepresentation = completedPostProcessingToken != null
1539                                        && response.isPostProcessedFor(completedPostProcessingToken)
1540                                        && response.getStatusCode().equals(headRepresentation.getStatusCode())
1541                                        && Objects.equals(response.getHeaders().get("Content-Encoding"), headRepresentation.getHeaders().get("Content-Encoding"))
1542                                        && (projectedLength == null || headRepresentation.getBody().isEmpty()
1543                                                        || projectedLength.equals(List.of(Long.toString(headRepresentation.getBodyLength()))));
1544                        if (!preservesRepresentation || response.getBody().isPresent() || response.isStreaming())
1545                                headRepresentation = response;
1546                }
1547                headRepresentationConsumer.accept(headRepresentation);
1548                if (completedPostProcessingToken != null)
1549                        response = response.markPostProcessed(completedPostProcessingToken);
1550                response = normalizeBodylessMarshaledResponse(request, response);
1551                if (!suppressContentLength) response = applyContentLengthIfApplicable(request, response);
1552                if (!response.getHeaders().containsKey("Date"))
1553                        response = response.copy().headers(headers -> headers.put("Date", List.of(HttpDate.currentSecondHeaderValue()))).finish();
1554                return response;
1555        }
1556
1557        @NonNull
1558        private static MarshaledResponse normalizeBodylessMarshaledResponse(@NonNull Request request,
1559                        @NonNull MarshaledResponse marshaledResponse) {
1560                enforceMarshaledResponseStatusCode(marshaledResponse, false);
1561                if (isBodylessStatusCode(marshaledResponse.getStatusCode())) {
1562                        int statusCode = marshaledResponse.getStatusCode();
1563                        marshaledResponse = marshaledResponse.copy().withoutBody().withoutStreamingResponseBody().headers(headers -> {
1564                                headers.remove("Transfer-Encoding");
1565                                if (statusCode == 205)
1566                                        headers.put("Content-Length", List.of("0"));
1567                                else if (statusCode == 204 || request.getHttpMethod() != HttpMethod.HEAD)
1568                                        headers.remove("Content-Length");
1569                        }).finish();
1570                }
1571                return marshaledResponse;
1572        }
1573
1574        @NonNull
1575        MarshaledResponse applyContentLengthIfApplicable(@NonNull Request request,
1576                                                                                                                                                                                                                                                 @NonNull MarshaledResponse marshaledResponse) {
1577                requireNonNull(request);
1578                requireNonNull(marshaledResponse);
1579
1580                if (marshaledResponse.isStreaming())
1581                        return marshaledResponse;
1582
1583                Set<String> normalizedHeaderNames = marshaledResponse.getHeaders().keySet().stream()
1584                                .map(headerName -> headerName.toLowerCase(Locale.US))
1585                                .collect(Collectors.toSet());
1586
1587                // If Content-Length is already specified, don't do anything
1588                if (normalizedHeaderNames.contains("content-length") || normalizedHeaderNames.contains("transfer-encoding"))
1589                        return marshaledResponse;
1590
1591                if (shouldOmitAutomaticContentLength(request, marshaledResponse))
1592                        return marshaledResponse;
1593
1594                // If Content-Length is not specified, specify as the number of bytes in the body
1595                return marshaledResponse.copy()
1596                                .headers((mutableHeaders) -> {
1597                                        String contentLengthHeaderValue = String.valueOf(marshaledResponse.getBodyLength());
1598                                        mutableHeaders.put("Content-Length", List.of(contentLengthHeaderValue));
1599                                }).finish();
1600        }
1601
1602        private boolean shouldOmitAutomaticContentLength(@NonNull Request request,
1603                                                                                                                                                                                                         @NonNull MarshaledResponse marshaledResponse) {
1604                requireNonNull(request);
1605                requireNonNull(marshaledResponse);
1606
1607                int statusCode = marshaledResponse.getStatusCode();
1608
1609                if ((statusCode >= 100 && statusCode < 200) || statusCode == 204 || statusCode == 304)
1610                        return true;
1611
1612                return statusCode != 205 && request.getHttpMethod() == HttpMethod.HEAD && marshaledResponse.getBodyLength() == 0L;
1613        }
1614
1615        @NonNull
1616        MarshaledResponse applyCorsResponseIfApplicable(@NonNull Request request,
1617                                                                                                                                                                                                                                                @NonNull MarshaledResponse marshaledResponse) {
1618                requireNonNull(request);
1619                requireNonNull(marshaledResponse);
1620
1621                CorsAuthorizer corsAuthorizer = getSokletConfig().getCorsAuthorizer();
1622                // Include every cache variant, even when this request omits Origin or is denied.
1623                if (!sameInstance(corsAuthorizer, CorsAuthorizer.rejectAllInstance()) && request.getCorsPreflight().isEmpty())
1624                        marshaledResponse = marshaledResponse.copy().headers(headers -> Utilities.addVaryHeader(headers, "Origin")).finish();
1625
1626                Cors cors = request.getCors().orElse(null);
1627
1628                // If non-CORS request, nothing further to do (note that CORS preflight was handled earlier)
1629                if (cors == null || !Cors.isSafeToReflectOrigin(cors.getOrigin()))
1630                        return marshaledResponse;
1631
1632                // Does the authorizer say we are authorized?
1633                CorsResponse corsResponse = corsAuthorizer.authorize(request, cors).orElse(null);
1634
1635                // Not authorized - don't apply CORS headers to the response
1636                if (corsResponse == null)
1637                        return marshaledResponse;
1638
1639                // Authorized - OK, let's apply the headers to the response
1640                return getSokletConfig().getResponseMarshaler().forCorsAllowed(request, cors, corsResponse, marshaledResponse);
1641        }
1642
1643        @NonNull
1644        Map<@NonNull HttpMethod, @NonNull ResourceMethod> resolveMatchingResourceMethodsByHttpMethod(@NonNull Request request,
1645                                                                                                                                                                                                                                                                                                                                                                                                                                 @NonNull ResourceMethodResolver resourceMethodResolver,
1646                                                                                                                                                                                                                                                                                                                                                                                                                                 @NonNull ServerType serverType) {
1647                requireNonNull(request);
1648                requireNonNull(resourceMethodResolver);
1649                requireNonNull(serverType);
1650
1651                // Special handling for OPTIONS *
1652                if (isOptionsSplat(request.getResourcePath()))
1653                        return new LinkedHashMap<>();
1654
1655                Map<HttpMethod, ResourceMethod> matchingResourceMethodsByHttpMethod = new LinkedHashMap<>(HttpMethod.values().length);
1656
1657                for (HttpMethod httpMethod : HttpMethod.values()) {
1658                        // Make a quick copy of the request to see if other paths match
1659                        Request otherRequest = request.copy().httpMethod(httpMethod).finish();
1660                        ResourceMethod resourceMethod =
1661                                        validateResolvedResourceMethod(resourceMethodResolver
1662                                                        .resourceMethodForRequest(otherRequest, serverType)
1663                                                        .orElse(null));
1664
1665                        if (resourceMethod != null)
1666                                matchingResourceMethodsByHttpMethod.put(httpMethod, resourceMethod);
1667                }
1668
1669                return matchingResourceMethodsByHttpMethod;
1670        }
1671
1672        @Nullable
1673        private static ResourceMethod resolveResourceMethodForRequest(@NonNull Request request,
1674                        @NonNull ResourceMethodResolver resourceMethodResolver, @NonNull ServerType serverType) {
1675                ResourceMethod resourceMethod = validateResolvedResourceMethod(resourceMethodResolver
1676                                .resourceMethodForRequest(request, serverType).orElse(null));
1677                if (resourceMethod == null && request.getHttpMethod() == HttpMethod.HEAD && serverType == ServerType.HTTP) {
1678                        // Resolve the GET fallback before observers/interceptors see routing metadata.
1679                        // Only the lookup copy becomes GET; the application still receives HEAD.
1680                        Request getRequest = request.copy().httpMethod(HttpMethod.GET).finish();
1681                        resourceMethod = validateResolvedResourceMethod(resourceMethodResolver
1682                                        .resourceMethodForRequest(getRequest, serverType).orElse(null));
1683                }
1684                return resourceMethod;
1685        }
1686
1687        @Nullable
1688        private static ResourceMethod validateResolvedResourceMethod(
1689                        @Nullable ResourceMethod resourceMethod) {
1690                if (resourceMethod != null)
1691                        SokletFrameworkSetup.validateNoRemovedHttpServerInjection(
1692                                        resourceMethod);
1693                return resourceMethod;
1694        }
1695
1696        @SuppressWarnings("ReferenceEquality")
1697        private static @NonNull Boolean isOptionsSplat(@NonNull ResourcePath resourcePath) {
1698                requireNonNull(resourcePath);
1699                return resourcePath == ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH;
1700        }
1701
1702        @NonNull
1703        private static Set<@NonNull HttpMethod> allowedHttpMethodsForResponse(@NonNull Map<@NonNull HttpMethod, @NonNull ResourceMethod> matchingResourceMethodsByHttpMethod,
1704                                                                                                                                                                                                                                                                                                @NonNull Boolean includeOptions, @NonNull ServerType serverType) {
1705                requireNonNull(matchingResourceMethodsByHttpMethod);
1706                requireNonNull(includeOptions);
1707
1708                Set<HttpMethod> allowedHttpMethods = EnumSet.noneOf(HttpMethod.class);
1709                allowedHttpMethods.addAll(matchingResourceMethodsByHttpMethod.keySet());
1710
1711                if (includeOptions)
1712                        allowedHttpMethods.add(HttpMethod.OPTIONS);
1713
1714                if ((serverType == ServerType.HTTP && matchingResourceMethodsByHttpMethod.containsKey(HttpMethod.GET))
1715                                || matchingResourceMethodsByHttpMethod.containsKey(HttpMethod.HEAD))
1716                        allowedHttpMethods.add(HttpMethod.HEAD);
1717
1718                return allowedHttpMethods;
1719        }
1720
1721        @NonNull
1722        MarshaledResponse provideFailsafeMarshaledResponse(@NonNull Request request,
1723                                                                                                                                                                                                                                                         @NonNull Throwable throwable) {
1724                requireNonNull(request);
1725                requireNonNull(throwable);
1726
1727                Integer statusCode = 500;
1728                Charset charset = StandardCharsets.UTF_8;
1729
1730                return MarshaledResponse.withStatusCode(statusCode)
1731                                .headers(Map.of("Content-Type", List.of(format("text/plain; charset=%s", charset.name()))))
1732                                .body(format("HTTP %s: %s", statusCode, StatusCode.fromStatusCode(statusCode).get().getReasonPhrase()).getBytes(charset))
1733                                .build();
1734        }
1735
1736        /**
1737         * Publishes shutdown intent, joins the lifecycle uninterruptibly, restores
1738         * the caller's interrupt status, and applies the terminal result.
1739         *
1740         * @throws SokletShutdownIncompleteException if complete termination cannot be proven
1741         * @throws SokletUnexpectedTerminationException if a lifecycle component terminated
1742         * unexpectedly after readiness
1743         * @throws IllegalStateException as a best-effort diagnostic when the current
1744         * thread is contributing to the unpublished shutdown barrier
1745         */
1746        void close() {
1747                shutdown();
1748                boolean interrupted = false;
1749                ShutdownResult result;
1750
1751                try {
1752                        for (;;) {
1753                                try {
1754                                        result = awaitCoreShutdown();
1755                                        break;
1756                                } catch (InterruptedException exception) {
1757                                        interrupted = true;
1758                                }
1759                        }
1760
1761                        this.directLifecycle.throwIfUnsuccessfulShutdown(result);
1762                } finally {
1763                        if (interrupted)
1764                                Thread.currentThread().interrupt();
1765                }
1766        }
1767
1768        /**
1769         * Returns a racy diagnostic snapshot of this one-shot lifecycle.
1770         *
1771         * @return current lifecycle status
1772         */
1773        @NonNull
1774        SokletStatus getStatus() {
1775                return this.directLifecycle.publicStatus();
1776        }
1777
1778        /**
1779         * Returns the immutable terminal result after it has been published.
1780         *
1781         * @return terminal result, otherwise empty
1782         */
1783        @NonNull
1784        Optional<@NonNull ShutdownResult> getShutdownResult() {
1785                return this.directLifecycle.publicResult();
1786        }
1787
1788        void initializeForSimulator(@NonNull StartupContext startupContext,
1789                        @NonNull DeadlineWaiter waiter) {
1790                this.directLifecycle.initializeForSimulator(startupContext, waiter);
1791        }
1792
1793        @NonNull
1794        SokletDirectLifecycle getDirectLifecycle() {
1795                return this.directLifecycle;
1796        }
1797
1798        @NonNull
1799        SokletConfig getSokletConfig() {
1800                return this.sokletConfig;
1801        }
1802
1803        /**
1804         * Returns the stable compatibility lock retained from earlier Soklet
1805         * versions.  The one-shot lifecycle does not use this caller-obtainable lock
1806         * for its internal state transitions, so holding it cannot delay lifecycle
1807         * progress.
1808         *
1809         * @return passive compatibility lock
1810         */
1811        @NonNull
1812        ReentrantLock getLock() {
1813                return this.lock;
1814        }
1815
1816        /**
1817         * Returns the stable compatibility reference initially containing the latch
1818         * released when this one-shot lifecycle publishes its terminal result.
1819         * Soklet retains that original latch directly, so replacing the exposed
1820         * reference value cannot redirect lifecycle publication.
1821         *
1822         * @return one-shot shutdown-latch reference
1823         */
1824        @NonNull
1825        AtomicReference<@NonNull CountDownLatch>
1826                        getAwaitShutdownLatchReference() {
1827                return this.awaitShutdownLatchReference;
1828        }
1829
1830        void releaseAwaitShutdownLatch() {
1831                this.awaitShutdownLatch.countDown();
1832        }
1833
1834        @ThreadSafe
1835        static class DefaultSimulator implements Simulator {
1836                @Nullable
1837                private MockHttpServer server;
1838                @Nullable
1839                private MockSseServer sseServer;
1840                @NonNull
1841                private final SimulatorOptions simulatorOptions;
1842                private final @Nullable DefaultMcpServer mcpServer;
1843                private final @Nullable SimulatorScopeDispatchGate scopeDispatchGate;
1844                private final @Nullable Object executionOwnerToken;
1845                @NonNull
1846                private final Object mcpSimulationLock;
1847                @NonNull
1848                private final Object scopeStateLock;
1849                private volatile @Nullable SimulationSession mcpSimulationSession;
1850                private volatile @Nullable SimulationSession rejectedMcpSimulationSession;
1851                private volatile @Nullable StreamLifecycleCoordinator streamLifecycleCoordinator;
1852                private @Nullable Supplier<@NonNull StreamLifecycleCoordinator> streamLifecycleCoordinatorFactoryForTests;
1853                private volatile @Nullable StreamLifecycleCoordinator sseLifecycleCoordinator;
1854                private @Nullable Supplier<@NonNull StreamLifecycleCoordinator> sseLifecycleCoordinatorFactoryForTests;
1855                @NonNull
1856                private final AtomicBoolean closed;
1857
1858                public DefaultSimulator(@Nullable MockHttpServer server,
1859                                                                                         @Nullable MockSseServer sseServer) {
1860                        this(server, sseServer, SimulatorOptions.defaultInstance(), null, null);
1861                }
1862
1863                public DefaultSimulator(@Nullable MockHttpServer server,
1864                                                                                @Nullable MockSseServer sseServer,
1865                                                                                @NonNull SimulatorOptions simulatorOptions) {
1866                        this(server, sseServer, simulatorOptions, null, null);
1867                }
1868
1869                DefaultSimulator(@Nullable MockHttpServer server,
1870                                @Nullable MockSseServer sseServer,
1871                                @NonNull SimulatorOptions simulatorOptions,
1872                                @Nullable DefaultMcpServer mcpServer) {
1873                        this(server, sseServer, simulatorOptions, mcpServer, null);
1874                }
1875
1876                DefaultSimulator(@Nullable MockHttpServer server,
1877                                @Nullable MockSseServer sseServer,
1878                                @NonNull SimulatorOptions simulatorOptions,
1879                                @Nullable DefaultMcpServer mcpServer,
1880                                @Nullable SimulatorScopeDispatchGate scopeDispatchGate) {
1881                        this(server, sseServer, simulatorOptions, mcpServer, scopeDispatchGate, null);
1882                }
1883
1884                DefaultSimulator(@Nullable MockHttpServer server, @Nullable MockSseServer sseServer,
1885                                @NonNull SimulatorOptions simulatorOptions, @Nullable DefaultMcpServer mcpServer,
1886                                @Nullable SimulatorScopeDispatchGate scopeDispatchGate, @Nullable Object executionOwnerToken) {
1887                        this.server = server;
1888                        this.sseServer = sseServer;
1889                        this.simulatorOptions = requireNonNull(simulatorOptions);
1890                        this.mcpServer = mcpServer;
1891                        this.scopeDispatchGate = scopeDispatchGate;
1892                        this.executionOwnerToken = executionOwnerToken;
1893                        this.mcpSimulationLock = new Object();
1894                        this.scopeStateLock = new Object();
1895                        this.closed = new AtomicBoolean();
1896                }
1897
1898                private void runStreamCallback(@NonNull Runnable callback) {
1899                        Object token = this.executionOwnerToken;
1900                        try (LifecycleExecutionContext.Scope ignored = token == null ? null : LifecycleExecutionContext.enter(token)) {
1901                                callback.run();
1902                        }
1903                }
1904
1905                @Override
1906                @NonNull
1907                public McpSimulation startMcpRequest(@NonNull Request request) {
1908                        return startMcpRequest(requireNonNull(request),
1909                                        McpSimulationOptions.defaultInstance());
1910                }
1911
1912                @Override
1913                @NonNull
1914                public McpSimulation startMcpRequest(@NonNull Request request,
1915                                @NonNull McpSimulationOptions options) {
1916                        Runnable releaseScope = enterScope(InternalLifecycleComponentType.MCP);
1917                        try {
1918                                synchronized (this.mcpSimulationLock) {
1919                                        if (this.mcpServer == null)
1920                                                throw new IllegalStateException(
1921                                                                "You must specify a McpServer in your SokletConfig to simulate MCP requests");
1922                                        if (this.mcpSimulationSession == null
1923                                                        && this.scopeDispatchGate == null)
1924                                                this.mcpSimulationSession = this.mcpServer
1925                                                                .openSimulationSession();
1926                                        if (this.mcpSimulationSession == null)
1927                                                throw new IllegalStateException(
1928                                                                "The MCP simulator participant is not ready");
1929                                        return this.mcpSimulationSession.start(requireNonNull(request),
1930                                                        requireNonNull(options));
1931                                }
1932                        } finally {
1933                                releaseScope.run();
1934                        }
1935                }
1936
1937                void openMcpScope() {
1938                        synchronized (this.mcpSimulationLock) {
1939                                requireScopeOpenWhileLocked();
1940                                if (this.mcpServer == null)
1941                                        throw new IllegalStateException(
1942                                                        "The simulator scope has no MCP participant");
1943                                if (this.mcpSimulationSession != null)
1944                                        throw new IllegalStateException(
1945                                                        "The MCP simulator participant was already started");
1946                                this.mcpServer.openSimulationSession(session -> {
1947                                        synchronized (this.scopeStateLock) {
1948                                                if (this.mcpSimulationSession != null
1949                                                                || this.rejectedMcpSimulationSession != null)
1950                                                        throw new IllegalStateException(
1951                                                                        "The MCP simulator participant was already started");
1952                                                SimulationSession exactSession = requireNonNull(session);
1953                                                if (this.closed.get()) {
1954                                                        // The runtime will reject and roll back this unclaimed session,
1955                                                        // but custom execution or registration cleanup may finish later.
1956                                                        // Retain its proof handle without exposing it as an active session.
1957                                                        this.rejectedMcpSimulationSession = exactSession;
1958                                                        requireScopeOpenWhileLocked();
1959                                                }
1960                                                this.mcpSimulationSession = exactSession;
1961                                        }
1962                                });
1963                        }
1964                }
1965
1966                boolean sealScope() {
1967                        // Close dispatch admission without waiting for a startup call that may be
1968                        // inside the MCP session lock.  A request racing shutdown must lose at the
1969                        // owner gate, and startup cancellation must remain deadline-bounded.
1970                        synchronized (this.scopeStateLock) {
1971                                if (!this.closed.compareAndSet(false, true))
1972                                        return false;
1973                                if (this.scopeDispatchGate != null)
1974                                        this.scopeDispatchGate.seal();
1975                                return true;
1976                        }
1977                }
1978
1979                void quiesceMcpScope() {
1980                        SimulationSession session = mcpSimulationSession();
1981                        if (session != null)
1982                                session.quiesce();
1983                }
1984
1985                void quiesceHttpScope() {
1986                        StreamLifecycleCoordinator coordinator = this.streamLifecycleCoordinator;
1987                        if (coordinator != null)
1988                                coordinator.requestGracefulShutdown();
1989                }
1990
1991                void forceHttpScope() {
1992                        StreamLifecycleCoordinator coordinator = this.streamLifecycleCoordinator;
1993                        if (coordinator != null)
1994                                coordinator.force();
1995                }
1996
1997                boolean awaitHttpScopeTermination(long absoluteDeadlineNanos,
1998                                @NonNull NanoClock clock) throws InterruptedException {
1999                        StreamLifecycleCoordinator coordinator = this.streamLifecycleCoordinator;
2000                        if (coordinator == null)
2001                                return true;
2002                        long remaining = absoluteDeadlineNanos - requireNonNull(clock).nanoTime();
2003                        return coordinator.awaitTermination(System.nanoTime() + Math.max(0L, remaining));
2004                }
2005
2006                boolean httpScopeTerminationProven() {
2007                        StreamLifecycleCoordinator coordinator = this.streamLifecycleCoordinator;
2008                        return coordinator == null || coordinator.isTerminated();
2009                }
2010
2011                @NonNull
2012                Set<@NonNull InternalResidualActivityType> httpScopeResidualActivity() {
2013                        StreamLifecycleCoordinator coordinator = this.streamLifecycleCoordinator;
2014                        if (coordinator == null || coordinator.isTerminated())
2015                                return Set.of();
2016                        StreamLifecycleCoordinator.Snapshot snapshot = coordinator.snapshot();
2017                        EnumSet<InternalResidualActivityType> residual = EnumSet.noneOf(InternalResidualActivityType.class);
2018                        if (snapshot.reservations() > 0)
2019                                residual.add(InternalResidualActivityType.STREAM);
2020                        if (snapshot.queuedProducers() > 0 || snapshot.runningProducers() > 0 || snapshot.reservations() == 0)
2021                                residual.add(InternalResidualActivityType.EXECUTOR_TASK);
2022                        if (snapshot.callbacks() > 0 || snapshot.diagnostics() > 0 || snapshot.publisherLifetimes() > 0)
2023                                residual.add(InternalResidualActivityType.CALLBACK);
2024                        return Collections.unmodifiableSet(residual);
2025                }
2026
2027                void setStreamLifecycleCoordinatorFactoryForTests(@NonNull Supplier<@NonNull StreamLifecycleCoordinator> factory) {
2028                        synchronized (this.scopeStateLock) {
2029                                requireScopeOpenWhileLocked();
2030                                if (this.streamLifecycleCoordinator != null)
2031                                        throw new IllegalStateException("Streaming lifecycle coordinator was already created");
2032                                this.streamLifecycleCoordinatorFactoryForTests = requireNonNull(factory);
2033                        }
2034                }
2035
2036                @NonNull
2037                Optional<@NonNull StreamLifecycleCoordinator> getStreamLifecycleCoordinatorForTests() {
2038                        return Optional.ofNullable(this.streamLifecycleCoordinator);
2039                }
2040
2041                private StreamLifecycleCoordinator.@Nullable Reservation reserveHttpStream() {
2042                        synchronized (this.scopeStateLock) {
2043                                if (this.closed.get())
2044                                        return null;
2045                                if (this.streamLifecycleCoordinator == null) {
2046                                        MockHttpServer server = requireNonNull(this.server);
2047                                        LifecycleObserver observer = server.getSokletConfig().orElseThrow().getAggregateLifecycleObserver();
2048                                        this.streamLifecycleCoordinator = this.streamLifecycleCoordinatorFactoryForTests == null
2049                                                        ? StreamLifecycleCoordinator.withCallbackExecution(server.streamingLifecycleCapacity,
2050                                                                        server.streamingCallbackConcurrency, server.streamingCleanupTimeout, throwable -> {
2051                                                                try {
2052                                                                        observer.didReceiveLogEvent(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR,
2053                                                                                        "A simulated streaming lifecycle supervision operation failed or exceeded its deadline")
2054                                                                                        .throwable(throwable).build());
2055                                                                } catch (Throwable observerFailure) {
2056                                                                        LifecycleObserverLogFallback.report(observerFailure);
2057                                                                }
2058                                                        }, this::runStreamCallback)
2059                                                        : requireNonNull(this.streamLifecycleCoordinatorFactoryForTests.get());
2060                                }
2061                                return this.streamLifecycleCoordinator.tryReserve();
2062                        }
2063                }
2064
2065                void quiesceSseScope() {
2066                        MockSseServer server = this.sseServer;
2067                        if (server != null)
2068                                server.stop();
2069                        StreamLifecycleCoordinator coordinator = this.sseLifecycleCoordinator;
2070                        if (coordinator != null)
2071                                coordinator.force();
2072                }
2073
2074                void forceSseScope() {
2075                        quiesceSseScope();
2076                }
2077
2078                boolean awaitSseScopeTermination(long absoluteDeadlineNanos,
2079                                @NonNull NanoClock clock) throws InterruptedException {
2080                        StreamLifecycleCoordinator coordinator = this.sseLifecycleCoordinator;
2081                        return coordinator == null || coordinator.awaitTermination(System.nanoTime()
2082                                        + Math.max(0L, absoluteDeadlineNanos - requireNonNull(clock).nanoTime()));
2083                }
2084
2085                boolean sseScopeTerminationProven() {
2086                        StreamLifecycleCoordinator coordinator = this.sseLifecycleCoordinator;
2087                        return coordinator == null || coordinator.isTerminated();
2088                }
2089
2090                @NonNull
2091                Set<@NonNull InternalResidualActivityType> sseScopeResidualActivity() {
2092                        StreamLifecycleCoordinator coordinator = this.sseLifecycleCoordinator;
2093                        if (coordinator == null || coordinator.isTerminated())
2094                                return Set.of();
2095                        StreamLifecycleCoordinator.Snapshot snapshot = coordinator.snapshot();
2096                        EnumSet<InternalResidualActivityType> residual = EnumSet.noneOf(InternalResidualActivityType.class);
2097                        if (snapshot.reservations() > 0)
2098                                residual.add(InternalResidualActivityType.STREAM);
2099                        if (snapshot.queuedProducers() > 0 || snapshot.runningProducers() > 0 || snapshot.reservations() == 0)
2100                                residual.add(InternalResidualActivityType.EXECUTOR_TASK);
2101                        if (snapshot.callbacks() > 0 || snapshot.diagnostics() > 0 || snapshot.retainedWork() > 0)
2102                                residual.add(InternalResidualActivityType.CALLBACK);
2103                        return Collections.unmodifiableSet(residual);
2104                }
2105
2106                void setSseLifecycleCoordinatorFactoryForTests(@NonNull Supplier<@NonNull StreamLifecycleCoordinator> factory) {
2107                        synchronized (this.scopeStateLock) {
2108                                requireScopeOpenWhileLocked();
2109                                if (this.sseLifecycleCoordinator != null)
2110                                        throw new IllegalStateException("SSE lifecycle coordinator was already created");
2111                                this.sseLifecycleCoordinatorFactoryForTests = requireNonNull(factory);
2112                        }
2113                }
2114
2115                @NonNull
2116                Optional<@NonNull StreamLifecycleCoordinator> getSseLifecycleCoordinatorForTests() {
2117                        return Optional.ofNullable(this.sseLifecycleCoordinator);
2118                }
2119
2120                private StreamLifecycleCoordinator.@Nullable Reservation reserveSseStream() {
2121                        synchronized (this.scopeStateLock) {
2122                                if (this.closed.get())
2123                                        return null;
2124                                return sseCoordinatorWhileLocked().tryReserve();
2125                        }
2126                }
2127
2128                private @NonNull StreamLifecycleCoordinator sseCoordinatorWhileLocked() {
2129                        if (this.sseLifecycleCoordinator == null) {
2130                                MockSseServer server = requireNonNull(this.sseServer);
2131                                LifecycleObserver observer = server.getSokletConfig().orElseThrow().getAggregateLifecycleObserver();
2132                                this.sseLifecycleCoordinator = this.sseLifecycleCoordinatorFactoryForTests == null
2133                                                ? StreamLifecycleCoordinator.withCallbackExecution(server.streamingLifecycleCapacity,
2134                                                                DefaultSseServer.STREAMING_COORDINATOR_CALLBACK_CONCURRENCY,
2135                                                                DefaultSseServer.STREAMING_COORDINATOR_CLEANUP_GRACE, throwable -> {
2136                                                        try {
2137                                                                observer.didReceiveLogEvent(LogEvent.with(LogEventType.SSE_SERVER_INTERNAL_ERROR,
2138                                                                                "A simulated SSE cleanup operation failed or exceeded its deadline")
2139                                                                                .throwable(throwable).build());
2140                                                        } catch (Throwable observerFailure) {
2141                                                                LifecycleObserverLogFallback.report(observerFailure);
2142                                                        }
2143                                                }, this::runStreamCallback) : requireNonNull(this.sseLifecycleCoordinatorFactoryForTests.get());
2144                        }
2145                        return this.sseLifecycleCoordinator;
2146                }
2147
2148                void forceMcpScope() {
2149                        SimulationSession session = mcpSimulationSession();
2150                        if (session != null)
2151                                session.force();
2152                }
2153
2154                boolean awaitMcpScopeTermination(long absoluteDeadlineNanos,
2155                                @NonNull NanoClock clock) throws InterruptedException {
2156                        SimulationSession session = mcpEvidenceSession();
2157                        return session == null
2158                                        || session.awaitTermination(absoluteDeadlineNanos,
2159                                                        requireNonNull(clock)::nanoTime);
2160                }
2161
2162                boolean mcpScopeTerminationProven() {
2163                        SimulationSession session = mcpEvidenceSession();
2164                        return session == null || session.terminationProven();
2165                }
2166
2167                @NonNull
2168                Set<@NonNull InternalResidualActivityType> mcpScopeResidualActivity() {
2169                        SimulationSession session = mcpEvidenceSession();
2170                        if (session == null)
2171                                return Set.of();
2172                        com.soklet.internal.mcp.protocol.McpServerRuntimeBridge.LifecycleEvidence
2173                                        evidence = session.lifecycleEvidence();
2174                        EnumSet<InternalResidualActivityType> residual =
2175                                        EnumSet.noneOf(InternalResidualActivityType.class);
2176                        if (evidence.executorTask() || evidence.subscriptionRegistration())
2177                                residual.add(InternalResidualActivityType.EXECUTOR_TASK);
2178                        if (evidence.stream())
2179                                residual.add(InternalResidualActivityType.STREAM);
2180                        if (evidence.callback())
2181                                residual.add(InternalResidualActivityType.CALLBACK);
2182                        return Collections.unmodifiableSet(residual);
2183                }
2184
2185                void releaseMcpScopeEvidence() {
2186                        SimulationSession activeSession = mcpSimulationSession();
2187                        SimulationSession rejectedSession = rejectedMcpSimulationSession();
2188                        if (activeSession != null)
2189                                activeSession.releaseLifecycleEvidence();
2190                        if (rejectedSession != null && rejectedSession != activeSession)
2191                                rejectedSession.releaseLifecycleEvidence();
2192                        clearMcpSimulationSession(activeSession);
2193                        clearRejectedMcpSimulationSession(rejectedSession);
2194                }
2195
2196                void releaseHttpAndSseScopeState() {
2197                        MockHttpServer httpServer = this.server;
2198                        MockSseServer serverSentEventsServer = this.sseServer;
2199                        if (httpServer != null)
2200                                httpServer.releaseSimulationScopeState();
2201                        if (serverSentEventsServer != null)
2202                                serverSentEventsServer.releaseSimulationScopeState();
2203                }
2204
2205                @Nullable
2206                private SimulationSession mcpSimulationSession() {
2207                        return this.mcpSimulationSession;
2208                }
2209
2210                @Nullable
2211                private SimulationSession rejectedMcpSimulationSession() {
2212                        return this.rejectedMcpSimulationSession;
2213                }
2214
2215                @Nullable
2216                private SimulationSession mcpEvidenceSession() {
2217                        SimulationSession activeSession = mcpSimulationSession();
2218                        return activeSession == null
2219                                        ? rejectedMcpSimulationSession() : activeSession;
2220                }
2221
2222                private void clearMcpSimulationSession(@Nullable SimulationSession session) {
2223                        synchronized (this.mcpSimulationLock) {
2224                                if (this.mcpSimulationSession == session)
2225                                        this.mcpSimulationSession = null;
2226                        }
2227                }
2228
2229                private void clearRejectedMcpSimulationSession(
2230                                @Nullable SimulationSession session) {
2231                        synchronized (this.scopeStateLock) {
2232                                if (this.rejectedMcpSimulationSession == session)
2233                                        this.rejectedMcpSimulationSession = null;
2234                        }
2235                }
2236
2237                @NonNull
2238                @Override
2239                public HttpRequestResult performHttpRequest(@NonNull Request request) {
2240                        Runnable releaseScope = enterScope(InternalLifecycleComponentType.HTTP);
2241                        try (HttpTransportResponseFeedback feedback = HttpTransportResponseFeedback.create()) {
2242                                // A caller may concurrently reuse an immutable Request. Each simulated
2243                                // dispatch needs its own identity, shared by handling and stream callbacks.
2244                                return performHttpRequestWhileAdmitted(requireNonNull(request).copy().finish(), feedback);
2245                        } finally {
2246                                releaseScope.run();
2247                        }
2248                }
2249
2250                @NonNull
2251                private HttpRequestResult performHttpRequestWhileAdmitted(
2252                                @NonNull Request request, @NonNull HttpTransportResponseFeedback feedback) {
2253                        MockHttpServer server = this.server;
2254
2255                        if (server == null)
2256                                throw new IllegalStateException(format("You must specify a %s in your %s to simulate requests",
2257                                                HttpServer.class.getSimpleName(), SokletConfig.class.getSimpleName()));
2258
2259                        AtomicReference<@Nullable HttpRequestResult> requestResultHolder = new AtomicReference<>();
2260                        HttpServer.RequestHandler requestHandler = server.getRequestHandler().orElse(null);
2261
2262                        if (requestHandler == null)
2263                                throw new IllegalStateException("You must register a request handler prior to simulating requests");
2264
2265                        AtomicReference<StreamLifecycleCoordinator.@Nullable Reservation> reservationHolder = new AtomicReference<>();
2266                        Throwable primaryFailure = null;
2267                        Throwable terminalCleanupFailure = null;
2268                        HttpRequestResult completedResult;
2269                        try {
2270                                requestHandler.handleRequest(request, HttpTransportResponseFeedback.fromWriter(feedback, requestResult -> {
2271                                        // Simulated responses do not run transport compression or retain its private HEAD input.
2272                                        HttpRequestResult result = projectSimulatedHttpResponse(request, requestResult);
2273                                        if (result.getMarshaledResponse().isStreaming()) {
2274                                                Instant streamEstablishedAt = Instant.now();
2275                                                long streamEstablishedNanos = System.nanoTime();
2276                                                StreamLifecycleCoordinator.Reservation reservation = reserveHttpStream();
2277                                                if (reservation == null) {
2278                                                        result.getMarshaledResponse().close();
2279                                                        MarshaledResponse unavailable = DefaultHttpServer.provideDefaultStreamingAdmissionRejection();
2280                                                        requestResultHolder.set(result.copy().marshaledResponse(unavailable).response(null).finish());
2281                                                        Throwable rejection = new RejectedExecutionException(
2282                                                                        "Streaming lifecycle capacity is unavailable.");
2283                                                        StreamLifecycleCoordinator coordinator = this.streamLifecycleCoordinator;
2284                                                        StreamTerminationReason reason = this.closed.get() || coordinator == null || !coordinator.snapshot().accepting()
2285                                                                        ? StreamTerminationReason.SERVER_STOPPING : StreamTerminationReason.BACKPRESSURE;
2286                                                        Runnable notification = () -> notifyDidTerminateSimulatorResponseStream(request, result, Instant.now(),
2287                                                                        Duration.ZERO, reason, rejection);
2288                                                        if ((coordinator == null || !coordinator.dispatchRejectionObserver(notification))
2289                                                                        && reason != StreamTerminationReason.SERVER_STOPPING) {
2290                                                                try {
2291                                                                        server.getSokletConfig().orElseThrow().getAggregateLifecycleObserver().didReceiveLogEvent(
2292                                                                                                LogEvent.with(LogEventType.RESPONSE_STREAM_CANCELED,
2293                                                                                                        "Unadmitted stream rejection observer capacity was unavailable.").request(request).build());
2294                                                                } catch (Throwable observerFailure) {
2295                                                                        LifecycleObserverLogFallback.report(observerFailure);
2296                                                                }
2297                                                        }
2298                                                        // Synchronous replacement keeps didWrite/finish callbacks and metrics finite.
2299                                                        return HttpTransportResponseOutcome.replaced(unavailable, rejection, null);
2300                                                }
2301                                                reservationHolder.set(reservation);
2302                                                HttpResponseStreamObservation observation = result.getResponseStreamObservation();
2303                                                if (observation != null) {
2304                                                        SokletConfig config = server.getSokletConfig().orElseThrow();
2305                                                        LifecycleObserver observer = config.getAggregateLifecycleObserver();
2306                                                        observation.prepare(new DefaultStreamingResponseHandle(ServerType.HTTP, observation.getRequest(),
2307                                                                        result.getResourceMethod().orElse(null), result.getMarshaledResponse(), streamEstablishedAt),
2308                                                                        streamEstablishedNanos, config.getMetricsCollector(), observer, logEvent -> {
2309                                                                try { observer.didReceiveLogEvent(logEvent); }
2310                                                                catch (Throwable observerFailure) { LifecycleObserverLogFallback.report(observerFailure); }
2311                                                        });
2312                                                }
2313                                        }
2314                                        requestResultHolder.set(result);
2315                                        return HttpTransportResponseOutcome.accepted();
2316                                }));
2317                                completedResult = materializeStreamingResponse(request, requestResultHolder.get(), reservationHolder.get());
2318                                if (!completedResult.getMarshaledResponse().isStreaming())
2319                                        completedResult = materializeFiniteSimulationResult(completedResult);
2320                        } catch (RuntimeException | Error failure) {
2321                                primaryFailure = failure;
2322                                throw failure;
2323                        } finally {
2324                                // Also retire a reservation if handling fails before inline producer entry.
2325                                StreamLifecycleCoordinator.Reservation reservation = reservationHolder.get();
2326                                try {
2327                                        HttpRequestResult result = requestResultHolder.get();
2328                                        if (result != null) {
2329                                                try { result.getMarshaledResponse().close(); }
2330                                                catch (RuntimeException | Error cleanupFailure) {
2331                                                        if (primaryFailure != null && !sameInstance(primaryFailure, cleanupFailure)) primaryFailure.addSuppressed(cleanupFailure);
2332                                                        if (reservation != null) reservation.reportCleanupFailure(cleanupFailure);
2333                                                        else if (primaryFailure == null) terminalCleanupFailure = cleanupFailure;
2334                                                }
2335                                        }
2336                                } finally {
2337                                        if (reservation != null) reservation.complete();
2338                                }
2339                        }
2340                        if (terminalCleanupFailure instanceof RuntimeException runtime) throw runtime;
2341                        if (terminalCleanupFailure instanceof Error error) throw error;
2342                        return completedResult;
2343                }
2344
2345                private static @NonNull HttpRequestResult materializeFiniteSimulationResult(@NonNull HttpRequestResult result) {
2346                        MarshaledResponse response = result.getMarshaledResponse();
2347                        Throwable primary = null;
2348                        try {
2349                                byte[] bytes = response.bodyBytesOrNull();
2350                                MarshaledResponse snapshot = MarshaledResponse.withStatusCode(response.getStatusCode())
2351                                                .headers(response.getHeaders()).cookies(response.getCookies()).body(bytes).build();
2352                                return result.copy().marshaledResponse(snapshot).finish();
2353                        } catch (RuntimeException | Error failure) {
2354                                primary = failure;
2355                                throw failure;
2356                        } finally {
2357                                MarshaledResponse.closeChannels(response.ownedChannels(), primary);
2358                        }
2359                }
2360
2361                private @NonNull HttpRequestResult materializeAcceptedSseSimulationResult(@NonNull HttpRequestResult result) {
2362                        MarshaledResponse response = result.getMarshaledResponse();
2363                        Object token = this.executionOwnerToken;
2364                        try (LifecycleExecutionContext.Scope ignored = token == null ? null : LifecycleExecutionContext.enter(token)) {
2365                                Throwable primary = null;
2366                                try {
2367                                        // Accepted handshakes have no body. Keep their metadata without retaining
2368                                        // channel ownership after the headers have been materialized.
2369                                        MarshaledResponse snapshot = MarshaledResponse.withStatusCode(response.getStatusCode())
2370                                                        .headers(response.getHeaders()).cookies(response.getCookies()).build();
2371                                        return result.copy().marshaledResponse(snapshot).finish();
2372                                } catch (RuntimeException | Error failure) {
2373                                        primary = failure;
2374                                        throw failure;
2375                                } finally {
2376                                        MarshaledResponse.closeChannels(response.ownedChannels(), primary);
2377                                }
2378                        }
2379                }
2380
2381                private static @NonNull HttpRequestResult projectSimulatedHttpResponse(@NonNull Request request, @NonNull HttpRequestResult result) {
2382                        MarshaledResponse response = normalizeBodylessMarshaledResponse(request, result.getMarshaledResponse());
2383                        boolean head = request.getHttpMethod() == HttpMethod.HEAD;
2384                        if (!response.isStreaming())
2385                                response = response.copy().headers(DefaultHttpServer.simulatedFiniteResponseHeaders(request, response,
2386                                                result.getHeadResponseBodyLength().orElse(null))).finish();
2387
2388                        // The transport suppresses HEAD bytes even when an interceptor or custom
2389                        // HeadHandler leaves a body. Never execute a suppressed stream producer.
2390                        if (head)
2391                                response = response.copy().withoutBody().withoutStreamingResponseBody().finish();
2392                        return result.copy().marshaledResponse(response).headResponseCompressionBody(null).finish();
2393                }
2394
2395                private static @NonNull HttpRequestResult projectSimulatedSseRejection(@NonNull Request request, @NonNull HttpRequestResult result) {
2396                        MarshaledResponse response = normalizeBodylessMarshaledResponse(request, result.getMarshaledResponse());
2397                        boolean head = request.getHttpMethod() == HttpMethod.HEAD;
2398                        if (head)
2399                                response = response.copy().withoutBody().withoutStreamingResponseBody().finish();
2400                        int status = response.getStatusCode();
2401                        long length = response.getBodyLength();
2402                        Set<String> transportHeaders = new HashSet<>(Set.of("connection", "keep-alive", "content-length",
2403                                        "transfer-encoding", "te", "trailer", "upgrade", "proxy-connection"));
2404                        for (String value : response.getHeaders().getOrDefault("Connection", List.of()))
2405                                for (String name : value.split(",", -1))
2406                                        transportHeaders.add(name.trim().toLowerCase(Locale.ROOT));
2407                        response = response.copy().headers(headers -> {
2408                                headers.keySet().removeIf(name -> transportHeaders.contains(name.toLowerCase(Locale.ROOT)));
2409                                if (!head && status != 204 && status != 304)
2410                                        headers.put("Content-Length", List.of(Long.toString(length)));
2411                                headers.put("Connection", List.of("close"));
2412                                headers.putIfAbsent("Date", List.of(HttpDate.currentSecondHeaderValue()));
2413                        }).finish();
2414                        return result.copy().marshaledResponse(response).headResponseCompressionBody(null).headResponseBodyLength(null).finish();
2415                }
2416
2417                @NonNull
2418                private HttpRequestResult materializeStreamingResponse(@NonNull Request request,
2419                                @Nullable HttpRequestResult requestResult, StreamLifecycleCoordinator.@Nullable Reservation reservedLifetime) {
2420                        requireNonNull(request);
2421
2422                        if (requestResult == null)
2423                                throw new IllegalStateException("No HTTP request result was produced by the simulator");
2424
2425                        StreamingResponseBody stream = requestResult.getMarshaledResponse().getStreamingResponseBody().orElse(null);
2426
2427                        if (stream == null)
2428                                return requestResult;
2429
2430                        StreamLifecycleCoordinator.Reservation reservation = requireNonNull(reservedLifetime);
2431                        AtomicReference<@Nullable HttpRequestResult> result = new AtomicReference<>();
2432                        AtomicReference<@Nullable Throwable> failure = new AtomicReference<>();
2433                        LifecycleObserver cleanupObserver = requireNonNull(this.server).getSokletConfig().orElseThrow()
2434                                        .getAggregateLifecycleObserver();
2435                        reservation.bindCleanupFailureObserver(throwable -> {
2436                                try {
2437                                        cleanupObserver.didReceiveLogEvent(LogEvent.with(LogEventType.RESPONSE_STREAM_CLOSE_FAILED,
2438                                                                "A simulated streaming response cleanup operation failed")
2439                                                        .throwable(throwable).request(request)
2440                                                        .resourceMethod(requestResult.getResourceMethod().orElse(null))
2441                                                        .marshaledResponse(requestResult.getMarshaledResponse()).build());
2442                                } catch (Throwable observerFailure) {
2443                                        LifecycleObserverLogFallback.report(observerFailure);
2444                                }
2445                        });
2446                        boolean entered = reservation.executeInline(() -> {
2447                                try {
2448                                        result.set(materializeStreamingResponseWhileReserved(request, requestResult, stream, reservation));
2449                                } catch (Throwable throwable) {
2450                                        failure.set(throwable);
2451                                } finally {
2452                                        reservation.complete();
2453                                }
2454                        });
2455                        if (!entered) {
2456                                StreamTerminationReason reason = reservation.reason().orElse(StreamTerminationReason.SERVER_STOPPING);
2457                                Throwable cause = reservation.cause().orElse(null);
2458                                try {
2459                                        notifyDidTerminateSimulatorResponseStream(reservation, request, requestResult, Instant.now(),
2460                                                        Duration.ZERO, reason, cause);
2461                                } finally {
2462                                        reservation.complete();
2463                                }
2464                                throw new IllegalStateException("Simulated streaming response was canceled: " + reason.name(),
2465                                                new StreamingResponseCanceledException(reason, cause));
2466                        }
2467                        Throwable throwable = failure.get();
2468                        if (throwable instanceof RuntimeException exception)
2469                                throw exception;
2470                        if (throwable instanceof Error error)
2471                                throw error;
2472                        if (throwable != null)
2473                                throw new IllegalStateException("Simulated streaming response failed.", throwable);
2474                        return requireNonNull(result.get());
2475                }
2476
2477                @NonNull
2478                private HttpRequestResult materializeStreamingResponseWhileReserved(@NonNull Request request,
2479                                @NonNull HttpRequestResult requestResult, @NonNull StreamingResponseBody stream,
2480                                StreamLifecycleCoordinator.@NonNull Reservation reservation) {
2481
2482                        byte[] bytes;
2483                        java.util.concurrent.atomic.AtomicLong acceptedBytes = new java.util.concurrent.atomic.AtomicLong();
2484                        Instant streamStarted = Instant.now();
2485
2486                        try {
2487                                bytes = materializeStreamingResponseBody(request, requestResult, stream, reservation, acceptedBytes);
2488                                if (!reservation.completeTransport())
2489                                        throw new StreamingResponseCanceledException(reservation.reason().orElse(StreamTerminationReason.SERVER_STOPPING),
2490                                                        reservation.cause().orElse(null));
2491                                notifyDidTerminateSimulatorResponseStream(reservation, request, requestResult, streamStarted,
2492                                                Duration.between(streamStarted, Instant.now()), null, null, acceptedBytes.get());
2493                        } catch (StreamingResponseCanceledException e) {
2494                                StreamTerminationReason cancelationReason = e.getCancelationReason();
2495                                Throwable cause = e.getCancelationCause().orElse(null);
2496                                notifyDidTerminateSimulatorResponseStream(reservation, request, requestResult, streamStarted,
2497                                                Duration.between(streamStarted, Instant.now()), cancelationReason, cause, acceptedBytes.get());
2498                                throw new IllegalStateException("Simulated streaming response was canceled: " + cancelationReason.name(), e);
2499                        } catch (InterruptedException e) {
2500                                Thread.currentThread().interrupt();
2501                                notifyDidTerminateSimulatorResponseStream(reservation, request, requestResult, streamStarted,
2502                                                Duration.between(streamStarted, Instant.now()), StreamTerminationReason.APPLICATION_CANCELED, e, acceptedBytes.get());
2503                                throw new IllegalStateException("Simulated streaming response was canceled: APPLICATION_CANCELED", e);
2504                        } catch (Throwable t) {
2505                                notifyDidTerminateSimulatorResponseStream(reservation, request, requestResult, streamStarted,
2506                                                Duration.between(streamStarted, Instant.now()), StreamTerminationReason.PRODUCER_FAILED, t, acceptedBytes.get());
2507
2508                                if (t instanceof Error error)
2509                                        throw error;
2510
2511                                throw new IllegalStateException("Simulated streaming response failed.", t);
2512                        }
2513
2514                        try { requestResult.getMarshaledResponse().close(); }
2515                        catch (RuntimeException | Error cleanupFailure) { reservation.reportCleanupFailure(cleanupFailure); }
2516                        MarshaledResponse marshaledResponse = requestResult.getMarshaledResponse().copy()
2517                                        .withoutStreamingResponseBody()
2518                                        .body(bytes)
2519                                        .finish();
2520
2521                        return requestResult.copy()
2522                                        .marshaledResponse(marshaledResponse)
2523                                        .finish();
2524                }
2525
2526                private void notifyDidTerminateSimulatorResponseStream(StreamLifecycleCoordinator.@NonNull Reservation reservation,
2527                                @NonNull Request request, @NonNull HttpRequestResult requestResult,
2528                                @NonNull Instant establishedAt, @NonNull Duration streamDuration,
2529                                @Nullable StreamTerminationReason cancelationReason, @Nullable Throwable throwable) {
2530                        notifyDidTerminateSimulatorResponseStream(reservation, request, requestResult, establishedAt,
2531                                        streamDuration, cancelationReason, throwable, 0L);
2532                }
2533
2534                private void notifyDidTerminateSimulatorResponseStream(StreamLifecycleCoordinator.@NonNull Reservation reservation,
2535                                @NonNull Request request, @NonNull HttpRequestResult requestResult,
2536                                @NonNull Instant establishedAt, @NonNull Duration streamDuration,
2537                                @Nullable StreamTerminationReason cancelationReason, @Nullable Throwable throwable, long bodyBytes) {
2538                        long terminalNanos = reservation.transportTerminatedNanos();
2539                        CountDownLatch delivered = new CountDownLatch(1);
2540                        reservation.dispatchTermination(() -> {
2541                                try {
2542                                        notifyDidTerminateSimulatorResponseStream(request, requestResult, establishedAt,
2543                                                        streamDuration, cancelationReason, throwable, terminalNanos, bodyBytes);
2544                                } finally {
2545                                        delivered.countDown();
2546                                }
2547                        });
2548                        boolean interrupted = false;
2549                        try {
2550                                for (;;) {
2551                                        try {
2552                                                delivered.await();
2553                                                return;
2554                                        } catch (InterruptedException ignored) {
2555                                                interrupted = true;
2556                                        }
2557                                }
2558                        } finally {
2559                                if (interrupted)
2560                                        Thread.currentThread().interrupt();
2561                        }
2562                }
2563
2564                private void notifyDidTerminateSimulatorResponseStream(@NonNull Request request,
2565                                                                                                                                                                                                                                         @NonNull HttpRequestResult requestResult,
2566                                                                                                                                                                                                                                         @NonNull Instant establishedAt,
2567                                                                                                                                                                                                                                         @NonNull Duration streamDuration,
2568                                                                                                                                                                                                                                         @Nullable StreamTerminationReason cancelationReason,
2569                                                                                                                                                                                                                                         @Nullable Throwable throwable) {
2570                        notifyDidTerminateSimulatorResponseStream(request, requestResult, establishedAt, streamDuration,
2571                                        cancelationReason, throwable, 0L, 0L);
2572                }
2573
2574                private void notifyDidTerminateSimulatorResponseStream(@NonNull Request request,
2575                                                                                                                                                                                                                                         @NonNull HttpRequestResult requestResult,
2576                                                                                                                                                                                                                                         @NonNull Instant establishedAt,
2577                                                                                                                                                                                                                                         @NonNull Duration streamDuration,
2578                                                                                                                                                                                                                                         @Nullable StreamTerminationReason cancelationReason,
2579                                                                                                                                                                                                                                         @Nullable Throwable throwable, long terminalNanos, long bodyBytes) {
2580                        requireNonNull(request);
2581                        requireNonNull(requestResult);
2582                        requireNonNull(establishedAt);
2583                        requireNonNull(streamDuration);
2584
2585                        MockHttpServer server = this.server;
2586                        SokletConfig sokletConfig = server == null ? null : server.getSokletConfig().orElse(null);
2587
2588                        if (sokletConfig == null)
2589                                return;
2590
2591                        MarshaledResponse marshaledResponse = requestResult.getMarshaledResponse();
2592                        ResourceMethod resourceMethod = requestResult.getResourceMethod().orElse(null);
2593                        LifecycleObserver lifecycleObserver = sokletConfig.getAggregateLifecycleObserver();
2594                        HttpResponseStreamObservation observation = requestResult.getResponseStreamObservation();
2595                        StreamingResponseHandle streamingResponse = new DefaultStreamingResponseHandle(ServerType.HTTP,
2596                                        observation == null ? request : observation.getRequest(), resourceMethod, marshaledResponse, establishedAt);
2597                        StreamTermination termination = StreamTermination
2598                                        .with(cancelationReason == null ? StreamTerminationReason.COMPLETED : cancelationReason, streamDuration)
2599                                        .cause(throwable)
2600                                        .build();
2601
2602                        if (observation != null && terminalNanos != 0L) {
2603                                HttpResponseStreamObservation.Delivery delivery = observation.deliver(streamingResponse, termination, terminalNanos, bodyBytes,
2604                                                sokletConfig.getMetricsCollector(), logEvent -> {
2605                                        try { lifecycleObserver.didReceiveLogEvent(logEvent); }
2606                                        catch (Throwable failure) { LifecycleObserverLogFallback.report(failure); }
2607                                });
2608                                streamingResponse = delivery.handle();
2609                                termination = delivery.termination();
2610                        }
2611                        if (cancelationReason != null && cancelationReason != StreamTerminationReason.CLIENT_DISCONNECTED
2612                                        && cancelationReason != StreamTerminationReason.SERVER_STOPPING) {
2613                                LogEventType type = cancelationReason == StreamTerminationReason.PRODUCER_FAILED
2614                                                ? LogEventType.RESPONSE_STREAM_FAILED : LogEventType.RESPONSE_STREAM_CANCELED;
2615                                try {
2616                                        lifecycleObserver.didReceiveLogEvent(LogEvent.with(type,
2617                                                        format("Streaming response terminated: %s", cancelationReason.name()))
2618                                                .throwable(throwable).request(request).resourceMethod(resourceMethod)
2619                                                .marshaledResponse(marshaledResponse).build());
2620                                } catch (Throwable observerFailure) {
2621                                        LifecycleObserverLogFallback.report(observerFailure);
2622                                }
2623                        }
2624
2625                        try {
2626                                lifecycleObserver.willTerminateResponseStream(streamingResponse, termination);
2627                        } catch (Throwable t) {
2628                                try {
2629                                        lifecycleObserver.didReceiveLogEvent(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_WILL_TERMINATE_RESPONSE_STREAM_FAILED,
2630                                                                        format("An exception occurred while invoking %s::willTerminateResponseStream", LifecycleObserver.class.getSimpleName()))
2631                                                        .throwable(t)
2632                                                        .request(request)
2633                                                        .resourceMethod(resourceMethod)
2634                                                        .marshaledResponse(marshaledResponse)
2635                                                        .build());
2636                                } catch (Throwable observerFailure) {
2637                                        LifecycleObserverLogFallback.report(observerFailure);
2638                                }
2639                        }
2640
2641                        try {
2642                                lifecycleObserver.didTerminateResponseStream(streamingResponse, termination);
2643                        } catch (Throwable t) {
2644                                try {
2645                                        lifecycleObserver.didReceiveLogEvent(LogEvent.with(LogEventType.LIFECYCLE_OBSERVER_DID_TERMINATE_RESPONSE_STREAM_FAILED,
2646                                                                        format("An exception occurred while invoking %s::didTerminateResponseStream", LifecycleObserver.class.getSimpleName()))
2647                                                        .throwable(t)
2648                                                        .request(request)
2649                                                        .resourceMethod(resourceMethod)
2650                                                        .marshaledResponse(marshaledResponse)
2651                                                        .build());
2652                                } catch (Throwable observerFailure) {
2653                                        LifecycleObserverLogFallback.report(observerFailure);
2654                                }
2655                        }
2656                }
2657
2658                private void notifyDidReceiveSimulatorStreamCancelationCallbackFailure(@NonNull Request request,
2659                                                                                                                                                                                                                                                                                                        @NonNull HttpRequestResult requestResult,
2660                                                                                                                                                                                                                                                                                                        @NonNull Throwable throwable) {
2661                        requireNonNull(request);
2662                        requireNonNull(requestResult);
2663                        requireNonNull(throwable);
2664
2665                        MockHttpServer server = this.server;
2666                        SokletConfig sokletConfig = server == null ? null : server.getSokletConfig().orElse(null);
2667
2668                        if (sokletConfig == null)
2669                                return;
2670
2671                        LifecycleObserver lifecycleObserver = sokletConfig.getAggregateLifecycleObserver();
2672                        ResourceMethod resourceMethod = requestResult.getResourceMethod().orElse(null);
2673                        MarshaledResponse marshaledResponse = requestResult.getMarshaledResponse();
2674
2675                        try {
2676                                lifecycleObserver.didReceiveLogEvent(LogEvent.with(LogEventType.RESPONSE_STREAM_CANCELATION_CALLBACK_FAILED,
2677                                                                "An exception occurred while invoking a streaming response cancelation callback")
2678                                                .throwable(throwable)
2679                                                .request(request)
2680                                                .resourceMethod(resourceMethod)
2681                                                .marshaledResponse(marshaledResponse)
2682                                                .build());
2683                        } catch (Throwable observerFailure) {
2684                                LifecycleObserverLogFallback.report(observerFailure);
2685                        }
2686                }
2687
2688                private byte @NonNull [] materializeStreamingResponseBody(@NonNull Request request,
2689                                                                                                                                                                                                                @NonNull HttpRequestResult requestResult,
2690                                @NonNull StreamingResponseBody stream,
2691                                StreamLifecycleCoordinator.@NonNull Reservation reservation, java.util.concurrent.atomic.@NonNull AtomicLong acceptedBytes) throws Exception {
2692                        requireNonNull(request);
2693                        requireNonNull(requestResult);
2694                        requireNonNull(stream);
2695
2696                        Consumer<Throwable> cleanupFailureConsumer = throwable ->
2697                                        notifyDidReceiveSimulatorStreamCancelationCallbackFailure(request, requestResult, throwable);
2698                        SimulatorCancelationToken cancelationToken = new SimulatorCancelationToken(cleanupFailureConsumer, reservation);
2699                        reservation.bindTermination(cancelationToken::deliverCancelation);
2700                        SimulatorResponseOutput output = new SimulatorResponseOutput(cancelationToken,
2701                                        getSimulatorOptions().getStreamingResponseBodyLimitInBytes(), acceptedBytes);
2702
2703                        try {
2704                                cancelationToken.throwIfCanceled();
2705                                if (stream instanceof StreamingResponseBody.PublisherBody publisherBody) {
2706                                        materializePublisher(publisherBody, output, cancelationToken, reservation);
2707                                } else {
2708                                        ManagedResponseStream managedResponseStream = new ManagedResponseStream(request, cancelationToken,
2709                                                        reservation::isGracefulShutdownRequested, null, null, output, reservation::beginCleanup,
2710                                                        throwable -> cancelSimulatorStream(cancelationToken, throwable), reservation::reportCleanupFailure);
2711                                        managedResponseStream.run(responseStream -> {
2712                                                if (stream instanceof StreamingResponseBody.WriterBody writerBody) {
2713                                                        writerBody.getWriter().writeTo(responseStream);
2714                                                } else if (stream instanceof StreamingResponseBody.InputStreamBody inputStreamBody) {
2715                                                        java.io.InputStream inputStream = responseStream.open(inputStreamBody.getInputStreamFactory());
2716                                                        byte[] buffer = new byte[inputStreamBody.getBufferSizeInBytes()];
2717                                                        int read;
2718
2719                                                        while ((read = inputStream.read(buffer)) >= 0) {
2720                                                                responseStream.getCancelationToken().throwIfCanceled();
2721                                                                if (read > 0)
2722                                                                        responseStream.write(ByteBuffer.wrap(buffer, 0, read));
2723                                                        }
2724                                                } else if (stream instanceof StreamingResponseBody.ReaderBody readerBody) {
2725                                                        Reader reader = responseStream.open(readerBody.getReaderFactory());
2726                                                        materializeReader(readerBody, reader, responseStream);
2727                                                } else {
2728                                                        throw new IllegalStateException(format("Unsupported streaming response body type: %s", stream.getClass().getName()));
2729                                                }
2730                                        });
2731                                }
2732                        } catch (Throwable t) {
2733                                cancelSimulatorStream(cancelationToken, t);
2734                                StreamTerminationReason reason = cancelationToken.getCancelationReason()
2735                                                .orElse(StreamTerminationReason.PRODUCER_FAILED);
2736                                if (reason != StreamTerminationReason.PRODUCER_FAILED
2737                                                && !(t instanceof InterruptedException && reason == StreamTerminationReason.APPLICATION_CANCELED)
2738                                                && !(t instanceof StreamingResponseCanceledException canceledException
2739                                                && canceledException.getCancelationReason() == reason)) {
2740                                        Throwable cause = cancelationToken.getCancelationCause().orElse(null);
2741                                        StreamingResponseCanceledException canceledException = new StreamingResponseCanceledException(reason, cause);
2742                                        if (!sameInstance(t, cause) && !(t instanceof InterruptedException))
2743                                                canceledException.addSuppressed(t);
2744                                        throw canceledException;
2745                                }
2746
2747                                if (t instanceof Exception exception)
2748                                        throw exception;
2749
2750                                if (t instanceof Error error)
2751                                        throw error;
2752
2753                                throw new RuntimeException(t);
2754                        }
2755
2756                        byte[] bytes = output.toByteArray();
2757                        if (!reservation.completeProduction())
2758                                cancelationToken.throwIfCanceled();
2759                        cancelationToken.complete();
2760                        return bytes;
2761                }
2762
2763                private void cancelSimulatorStream(@NonNull SimulatorCancelationToken cancelationToken,
2764                                @NonNull Throwable throwable) {
2765                        if (throwable instanceof StreamingResponseCanceledException canceledException) {
2766                                cancelationToken.cancel(canceledException.getCancelationReason(), canceledException.getCancelationCause().orElse(null));
2767                        } else if (throwable instanceof InterruptedException) {
2768                                Thread.currentThread().interrupt();
2769                                cancelationToken.cancel(cancelationToken.getCancelationReason()
2770                                                .orElse(StreamTerminationReason.APPLICATION_CANCELED), throwable);
2771                        } else {
2772                                cancelationToken.cancel(StreamTerminationReason.PRODUCER_FAILED, throwable);
2773                        }
2774                }
2775
2776                private void materializeReader(com.soklet.StreamingResponseBody.@NonNull ReaderBody readerBody,
2777                                                                                                                                         @NonNull Reader reader,
2778                                                                                                                                         @NonNull ResponseStream responseStream) throws IOException, InterruptedException, StreamingResponseCanceledException, CharacterCodingException {
2779                        requireNonNull(readerBody);
2780                        requireNonNull(reader);
2781                        requireNonNull(responseStream);
2782
2783                        CharsetEncoder encoder = readerBody.newEncoder();
2784                        int readSize = readerBody.getBufferSizeInCharacters();
2785                        // Preserve single-character reads while leaving room for a carried high surrogate.
2786                        CharBuffer charBuffer = CharBuffer.allocate(Math.max(2, readSize));
2787                        ByteBuffer byteBuffer = ByteBuffer.allocate(Math.max(128, (int) Math.ceil(readSize * encoder.maxBytesPerChar())));
2788
2789                        while (true) {
2790                                charBuffer.limit(charBuffer.position() + Math.min(readSize, charBuffer.remaining()));
2791                                int read = reader.read(charBuffer);
2792                                charBuffer.limit(charBuffer.capacity());
2793                                if (read < 0)
2794                                        break;
2795                                responseStream.getCancelationToken().throwIfCanceled();
2796                                charBuffer.flip();
2797                                encodeCharsForSimulator(encoder, charBuffer, byteBuffer, false, responseStream);
2798                                charBuffer.compact();
2799                        }
2800
2801                        charBuffer.flip();
2802                        encodeCharsForSimulator(encoder, charBuffer, byteBuffer, true, responseStream);
2803
2804                        CoderResult result;
2805                        do {
2806                                result = encoder.flush(byteBuffer);
2807                                writeEncodedBytesForSimulator(byteBuffer, responseStream);
2808                                if (result.isError())
2809                                        result.throwException();
2810                        } while (result.isOverflow());
2811                }
2812
2813                private void encodeCharsForSimulator(@NonNull CharsetEncoder encoder,
2814                                                                                                                                                                 @NonNull CharBuffer charBuffer,
2815                                                                                                                                                                 @NonNull ByteBuffer byteBuffer,
2816                                                                                                                                                                 boolean endOfInput,
2817                                                                                                                                                                 @NonNull ResponseStream output) throws IOException, InterruptedException, StreamingResponseCanceledException, CharacterCodingException {
2818                        CoderResult result;
2819
2820                        do {
2821                                result = encoder.encode(charBuffer, byteBuffer, endOfInput);
2822                                writeEncodedBytesForSimulator(byteBuffer, output);
2823
2824                                if (result.isError())
2825                                        result.throwException();
2826                        } while (result.isOverflow());
2827                }
2828
2829                private void writeEncodedBytesForSimulator(@NonNull ByteBuffer byteBuffer,
2830                                                                                                                                                                                         @NonNull ResponseStream output) throws IOException, InterruptedException, StreamingResponseCanceledException {
2831                        byteBuffer.flip();
2832                        if (byteBuffer.hasRemaining())
2833                                output.write(byteBuffer);
2834                        byteBuffer.clear();
2835                }
2836
2837                private void materializePublisher(com.soklet.StreamingResponseBody.@NonNull PublisherBody publisherBody,
2838                                @NonNull SimulatorResponseOutput output,
2839                                @NonNull SimulatorCancelationToken cancelationToken,
2840                                StreamLifecycleCoordinator.@NonNull Reservation reservation) throws Exception {
2841                        PublisherResponseStream.copy(publisherBody, cancelationToken, output, reservation,
2842                                        reservation::beginCleanup, throwable -> cancelSimulatorStream(cancelationToken, throwable),
2843                                        reservation::reportCleanupFailure);
2844                }
2845
2846                @NonNull
2847                @Override
2848                public SseRequestResult performSseRequest(@NonNull Request request) {
2849                        Runnable releaseScope = enterScope(InternalLifecycleComponentType.SSE);
2850                        try (HttpTransportResponseFeedback feedback = HttpTransportResponseFeedback.create()) {
2851                                return performSseRequestWhileAdmitted(requireNonNull(request).copy().finish(), feedback);
2852                        } finally {
2853                                releaseScope.run();
2854                        }
2855                }
2856
2857                @NonNull
2858                private SseRequestResult performSseRequestWhileAdmitted(
2859                                @NonNull Request request, @NonNull HttpTransportResponseFeedback feedback) {
2860                        MockSseServer sseServer = this.sseServer;
2861
2862                        if (sseServer == null)
2863                                throw new IllegalStateException(format("You must specify a %s in your %s to simulate Server-Sent Event requests",
2864                                                SseServer.class.getSimpleName(), SokletConfig.class.getSimpleName()));
2865
2866                        AtomicReference<@Nullable HttpRequestResult> requestResultHolder = new AtomicReference<>();
2867                        SseServer.RequestHandler requestHandler = sseServer.getRequestHandler().orElse(null);
2868                        AtomicReference<StreamLifecycleCoordinator.@Nullable Reservation> reservationHolder = new AtomicReference<>();
2869                        AtomicReference<SseConnection.@Nullable HandshakeFailureReason> replacementFailureReason = new AtomicReference<>();
2870                        AtomicReference<@Nullable Throwable> replacementFailureCause = new AtomicReference<>();
2871
2872                        if (requestHandler == null)
2873                                throw new IllegalStateException("You must register a request handler prior to simulating SSE Event Source requests");
2874                        if (request.getHttpMethod() == HttpMethod.GET) {
2875                                try {
2876                                        DefaultSseServer.validateSseRequestBody(request);
2877                                } catch (IllegalRequestException failure) {
2878                                        return rejectSseRequestBeforeHandling(request, sseServer, failure);
2879                                }
2880                        }
2881
2882                        requestHandler.handleRequest(request, HttpTransportResponseFeedback.fromWriter(feedback, requestResult -> {
2883                                HttpRequestResult effectiveResult = requestResult;
2884                                Throwable preparationFailure = null;
2885                                if (requestResult.getSseHandshakeResult().orElse(null) instanceof SseHandshakeResult.Accepted) {
2886                                        StreamLifecycleCoordinator.Reservation reservation = reserveSseStream();
2887                                        if (reservation == null) {
2888                                                replacementFailureReason.set(SseConnection.HandshakeFailureReason.CAPACITY_EXCEEDED);
2889                                                MarshaledResponse unavailable;
2890                                                try {
2891                                                        ResponseMarshaler marshaler = sseServer.getSokletConfig().orElseThrow().getResponseMarshaler();
2892                                                        unavailable = ResponseBodyCleanup.transform(
2893                                                                        marshaler.forServiceUnavailable(request, requestResult.getResourceMethod().orElse(null)),
2894                                                                        marshaler::postProcess);
2895                                                } catch (Throwable failure) {
2896                                                        preparationFailure = failure;
2897                                                        unavailable = DefaultSseServer.describeFailsafeHandshakeResponse(StatusCode.HTTP_500,
2898                                                                        request.getHttpMethod() == HttpMethod.HEAD);
2899                                                }
2900                                                effectiveResult = requestResult.copy().marshaledResponse(unavailable).response(null)
2901                                                                .sseHandshakeResult(null).finish();
2902                                        } else {
2903                                                reservationHolder.set(reservation);
2904                                        }
2905                                }
2906                                if (!(effectiveResult.getSseHandshakeResult().orElse(null) instanceof SseHandshakeResult.Accepted)
2907                                                && effectiveResult.getMarshaledResponse().isStreaming()) {
2908                                        preparationFailure = new IllegalArgumentException("SSE handshake rejection responses require a finite body");
2909                                        effectiveResult = effectiveResult.copy().sseHandshakeResult(null).response(null)
2910                                                        .marshaledResponse(DefaultSseServer.describeFailsafeHandshakeResponse(StatusCode.HTTP_500,
2911                                                                        request.getHttpMethod() == HttpMethod.HEAD)).finish();
2912                                }
2913                                if (preparationFailure != null) {
2914                                        replacementFailureReason.set(SseConnection.HandshakeFailureReason.INTERNAL_ERROR);
2915                                        replacementFailureCause.set(preparationFailure);
2916                                        sseServer.safelyLog(LogEvent.with(LogEventType.SSE_SERVER_INTERNAL_ERROR,
2917                                                        "Unable to generate SSE handshake response").throwable(preparationFailure).request(request)
2918                                                        .resourceMethod(requestResult.getResourceMethod().orElse(null)).build());
2919                                }
2920                                if (!sameInstance(effectiveResult, requestResult))
2921                                        effectiveResult = effectiveResult.copy().marshaledResponse(ResponseBodyCleanup.retain(
2922                                                        requestResult.getMarshaledResponse(), effectiveResult.getMarshaledResponse())).finish();
2923                                HttpRequestResult logicalResult = effectiveResult;
2924                                if (!(effectiveResult.getSseHandshakeResult().orElse(null) instanceof SseHandshakeResult.Accepted))
2925                                        effectiveResult = projectSimulatedSseRejection(request, effectiveResult);
2926                                requestResultHolder.set(effectiveResult);
2927                                if (!sameInstance(logicalResult, requestResult)) {
2928                                        return HttpTransportResponseOutcome.replaced(logicalResult.getMarshaledResponse(), preparationFailure, null);
2929                                }
2930                                return HttpTransportResponseOutcome.accepted();
2931                        }));
2932
2933                        HttpRequestResult requestResult = requestResultHolder.get();
2934                        if (requestResult == null)
2935                                throw new IllegalStateException("SSE request handler did not provide a request result");
2936
2937                        SseHandshakeResult sseHandshakeResult = requestResult.getSseHandshakeResult().orElse(null);
2938                        if (!(sseHandshakeResult instanceof SseHandshakeResult.Accepted))
2939                                requestResult = materializeFiniteSimulationResult(requestResult);
2940
2941                        if (sseHandshakeResult == null) {
2942                                Throwable cause = replacementFailureCause.get();
2943                                if (cause == null)
2944                                        cause = requestResult.getRequestHandlingFailure().orElse(null);
2945                                SseConnection.HandshakeFailureReason reason = replacementFailureReason.get();
2946                                if (reason == null)
2947                                        reason = cause == null ? SseConnection.HandshakeFailureReason.HANDSHAKE_REJECTED
2948                                                        : SseConnection.HandshakeFailureReason.INTERNAL_ERROR;
2949                                notifySseHandshakeFailure(request, requestResult, reason, cause);
2950                                return new SseRequestResult.RequestFailed(requestResult);
2951                        }
2952
2953                        if (sseHandshakeResult instanceof SseHandshakeResult.Accepted acceptedHandshake) {
2954                                SseClientInitializer clientInitializer = acceptedHandshake.getClientInitializer().orElse(null);
2955                                StreamLifecycleCoordinator.Reservation reservation = requireNonNull(reservationHolder.get());
2956
2957                                try {
2958                                        requestResult = materializeAcceptedSseSimulationResult(requestResult);
2959                                        // Create a synthetic logical response using values from the accepted handshake.
2960                                        if (requestResult.getResponse().isEmpty())
2961                                                requestResult = requestResult.copy()
2962                                                                .response(Response.withStatusCode(200)
2963                                                                                .headers(acceptedHandshake.getHeaders())
2964                                                                                .cookies(acceptedHandshake.getCookies()).build()).finish();
2965                                        HandshakeAccepted handshakeAccepted = new HandshakeAccepted(acceptedHandshake, request,
2966                                                        requestResult, sseServer, reservation);
2967                                        if (!handshakeAccepted.initialize(clientInitializer))
2968                                                throw new IllegalStateException("The simulated SSE connection terminated before activation");
2969                                        return handshakeAccepted;
2970                                } catch (Throwable failure) {
2971                                        // An initializer already elected PRODUCER_FAILED; construction/activation
2972                                        // failures need the same reservation cleanup without replacing that winner.
2973                                        reservation.cancel(StreamTerminationReason.INTERNAL_ERROR, failure);
2974                                        reservation.complete();
2975                                        if (failure instanceof RuntimeException runtimeException)
2976                                                throw runtimeException;
2977                                        if (failure instanceof Error error)
2978                                                throw error;
2979                                        throw new IllegalStateException("The simulated SSE client initializer failed", failure);
2980                                }
2981                        }
2982
2983                        if (sseHandshakeResult instanceof SseHandshakeResult.Rejected rejectedHandshake) {
2984                                notifySseHandshakeFailure(request, requestResult, SseConnection.HandshakeFailureReason.HANDSHAKE_REJECTED, null);
2985                                return new HandshakeRejected(rejectedHandshake, requestResult);
2986                        }
2987
2988                        throw new IllegalStateException(format("Encountered unexpected %s: %s", SseHandshakeResult.class.getSimpleName(), sseHandshakeResult));
2989                }
2990
2991                @NonNull
2992                private SseRequestResult rejectSseRequestBeforeHandling(@NonNull Request request,
2993                                @NonNull MockSseServer server, @NonNull IllegalRequestException failure) {
2994                        Object token = this.executionOwnerToken;
2995                        try (LifecycleExecutionContext.Scope ignored = token == null ? null : LifecycleExecutionContext.enter(token)) {
2996                                HttpRequestResult rejected = HttpRequestResult.withMarshaledResponse(MarshaledResponse.fromStatusCode(400))
2997                                                .requestHandlingFailure(failure).effectiveSseRequest(request).build();
2998                                // Live admission reports rejection even when its custom error marshaler fails.
2999                                notifySseHandshakeFailure(request, rejected, SseConnection.HandshakeFailureReason.HANDSHAKE_REJECTED, failure);
3000                                ResponseMarshaler marshaler = server.getSokletConfig().orElseThrow().getResponseMarshaler();
3001                                MarshaledResponse original = marshaler.forThrowable(request, failure, null);
3002                                MarshaledResponse response;
3003                                try {
3004                                        response = ResponseBodyCleanup.transform(original, marshaler::postProcess);
3005                                } catch (Throwable preparationFailure) {
3006                                        // The transform releases unchecked failures; JVM callbacks can also
3007                                        // emit checked failures that still need their owned channels closed.
3008                                        if (!(preparationFailure instanceof RuntimeException) && !(preparationFailure instanceof Error))
3009                                                MarshaledResponse.closeChannels(original.ownedChannels(), preparationFailure);
3010                                        throw preparationFailure;
3011                                }
3012                                boolean materializationOwnsCleanup = false;
3013                                try {
3014                                        enforceFinalResponseStatusCode(response.getStatusCode());
3015                                        if (response.isStreaming())
3016                                                throw new IllegalArgumentException("SSE handshake rejection responses require a finite body");
3017                                        HttpRequestResult result = projectSimulatedSseRejection(request,
3018                                                        rejected.copy().marshaledResponse(response).finish());
3019                                        // The materializer releases every owned channel, including when reading fails.
3020                                        materializationOwnsCleanup = true;
3021                                        return new SseRequestResult.RequestFailed(materializeFiniteSimulationResult(result));
3022                                } catch (Throwable preparationFailure) {
3023                                        if (!materializationOwnsCleanup)
3024                                                MarshaledResponse.closeChannels(response.ownedChannels(), preparationFailure);
3025                                        throw preparationFailure;
3026                                }
3027                        }
3028                }
3029
3030                private void notifySseHandshakeFailure(@NonNull Request request, @NonNull HttpRequestResult requestResult,
3031                                SseConnection.@NonNull HandshakeFailureReason reason, @Nullable Throwable cause) {
3032                        MockSseServer server = requireNonNull(this.sseServer);
3033                        StreamLifecycleCoordinator coordinator;
3034                        synchronized (this.scopeStateLock) {
3035                                if (this.closed.get())
3036                                        return;
3037                                coordinator = sseCoordinatorWhileLocked();
3038                        }
3039                        Request effectiveRequest = requestResult.getEffectiveSseRequest().orElse(request);
3040                        ResourceMethod resourceMethod = requestResult.getResourceMethod().orElse(null);
3041                        if (!coordinator.dispatchRejectionObserver(() -> server.notifyConnectionLifecycle(effectiveRequest, resourceMethod,
3042                                        LogEventType.LIFECYCLE_OBSERVER_DID_ESTABLISH_SSE_CONNECTION_FAILED, "didFailToEstablishSseConnection",
3043                                        observer -> observer.didFailToEstablishSseConnection(effectiveRequest, resourceMethod, reason, cause),
3044                                        metrics -> metrics.didFailToEstablishSseConnection(effectiveRequest, resourceMethod, reason, cause)))) {
3045                                server.safelyLog(LogEvent.with(LogEventType.SSE_SERVER_CONNECTION_REJECTED,
3046                                                "Simulated SSE handshake-failure observer capacity was unavailable.").request(request).build());
3047                        }
3048                }
3049
3050                @NonNull
3051                @Override
3052                public Simulator onBroadcastError(@Nullable Consumer<@NonNull Throwable> onBroadcastError) {
3053                        Runnable releaseScope = enterScope(InternalLifecycleComponentType.SSE);
3054                        try {
3055                                MockSseServer sseServer = this.sseServer;
3056                                if (sseServer != null)
3057                                        sseServer.onBroadcastError(onBroadcastError);
3058                                return this;
3059                        } finally {
3060                                releaseScope.run();
3061                        }
3062                }
3063
3064                @NonNull
3065                @Override
3066                public Simulator onUnicastError(@Nullable Consumer<@NonNull Throwable> onUnicastError) {
3067                        Runnable releaseScope = enterScope(InternalLifecycleComponentType.SSE);
3068                        try {
3069                                MockSseServer sseServer = this.sseServer;
3070                                if (sseServer != null)
3071                                        sseServer.onUnicastError(onUnicastError);
3072                                return this;
3073                        } finally {
3074                                releaseScope.run();
3075                        }
3076                }
3077
3078                @NonNull
3079                @Override
3080                public Optional<@NonNull HttpServer> getHttpServer() {
3081                        requireScopeOpen();
3082                        return Optional.ofNullable(this.server);
3083                }
3084
3085                @NonNull
3086                @Override
3087                public Optional<@NonNull SseServer> getSseServer() {
3088                        requireScopeOpen();
3089                        return Optional.ofNullable(this.sseServer);
3090                }
3091
3092                @NonNull
3093                @Override
3094                public Optional<@NonNull McpServer> getMcpServer() {
3095                        requireScopeOpen();
3096                        return Optional.ofNullable(this.mcpServer);
3097                }
3098
3099                @NonNull
3100                private Runnable enterScope(
3101                                @NonNull InternalLifecycleComponentType kind) {
3102                        requireScopeOpen();
3103                        return this.scopeDispatchGate == null
3104                                        ? () -> {
3105                                        }
3106                                        : this.scopeDispatchGate.enter(requireNonNull(kind));
3107                }
3108
3109                private void requireScopeOpen() {
3110                        requireScopeOpenWhileLocked();
3111                }
3112
3113                private void requireScopeOpenWhileLocked() {
3114                        if (this.closed.get())
3115                                throw new IllegalStateException("The simulator scope is closed.");
3116                }
3117
3118                @NonNull
3119                SimulatorOptions getSimulatorOptions() {
3120                        return this.simulatorOptions;
3121                }
3122        }
3123
3124        @NotThreadSafe
3125        private static final class SimulatorResponseOutput implements ManagedResponseStream.Output {
3126                @NonNull
3127                private final ByteArrayOutputStream byteArrayOutputStream;
3128                @NonNull
3129                private final Integer limitInBytes;
3130                @NonNull
3131                private final SimulatorCancelationToken cancelationToken;
3132                private boolean closed;
3133                private final java.util.concurrent.atomic.@NonNull AtomicLong acceptedBytes;
3134
3135                private SimulatorResponseOutput(@NonNull SimulatorCancelationToken cancelationToken,
3136                                @NonNull Integer limitInBytes, java.util.concurrent.atomic.@NonNull AtomicLong acceptedBytes) {
3137                        this.acceptedBytes = acceptedBytes;
3138                        this.byteArrayOutputStream = new ByteArrayOutputStream();
3139                        this.limitInBytes = requireNonNull(limitInBytes);
3140                        this.cancelationToken = requireNonNull(cancelationToken);
3141                }
3142
3143                @Override
3144                public void write(@NonNull ByteBuffer byteBuffer) throws IOException, StreamingResponseCanceledException {
3145                        requireNonNull(byteBuffer);
3146                        this.cancelationToken.throwIfCanceled();
3147
3148                        if (this.closed)
3149                                throw new StreamingResponseCanceledException(StreamTerminationReason.APPLICATION_CANCELED);
3150
3151                        ByteBuffer source = byteBuffer.asReadOnlyBuffer();
3152                        int bytesToWrite = source.remaining();
3153
3154                        if ((long) this.byteArrayOutputStream.size() + bytesToWrite > this.limitInBytes) {
3155                                this.cancelationToken.cancel(StreamTerminationReason.SIMULATOR_LIMIT_EXCEEDED, null);
3156                                throw new StreamingResponseCanceledException(StreamTerminationReason.SIMULATOR_LIMIT_EXCEEDED);
3157                        }
3158
3159                        byte[] bytes = new byte[bytesToWrite];
3160                        source.get(bytes);
3161                        Runnable append = () -> {
3162                                this.byteArrayOutputStream.writeBytes(bytes);
3163                                this.acceptedBytes.addAndGet(bytesToWrite);
3164                                byteBuffer.position(byteBuffer.position() + bytesToWrite);
3165                        };
3166                        StreamLifecycleCoordinator.Reservation reservation = this.cancelationToken.reservation;
3167                        if (reservation == null) append.run();
3168                        else if (!reservation.acceptTransportOutput(append)) this.cancelationToken.throwIfCanceled();
3169                }
3170
3171                @Override
3172                public int stagingCapacityInBytes() {
3173                        return Math.max(1, Math.min(8_192, this.limitInBytes));
3174                }
3175
3176                @Override
3177                public void flush() throws StreamingResponseCanceledException {
3178                        this.cancelationToken.throwIfCanceled();
3179                }
3180
3181                @Override
3182                public boolean isOpen() {
3183                        return !this.closed && !this.cancelationToken.isCanceled();
3184                }
3185
3186                private byte @NonNull [] toByteArray() {
3187                        this.closed = true;
3188                        return this.byteArrayOutputStream.toByteArray();
3189                }
3190        }
3191
3192        @ThreadSafe
3193        private static final class SimulatorCancelationToken implements CancelationToken {
3194                private static final @NonNull Runnable NO_CALLBACKS = () -> {};
3195                private final @NonNull Object lock = new Object();
3196                private boolean canceled;
3197                private boolean completed;
3198                private final StreamLifecycleCoordinator.@Nullable Reservation reservation;
3199                @Nullable
3200                private Set<@NonNull CancelationCallbackRegistration> callbacks;
3201                @NonNull
3202                private final Consumer<@NonNull Throwable> callbackFailureConsumer;
3203                @Nullable
3204                private StreamTerminationReason reason;
3205                @Nullable
3206                private Throwable cause;
3207
3208                private SimulatorCancelationToken(@NonNull Consumer<@NonNull Throwable> callbackFailureConsumer,
3209                                StreamLifecycleCoordinator.@Nullable Reservation reservation) {
3210                        this.callbackFailureConsumer = requireNonNull(callbackFailureConsumer);
3211                        this.reservation = reservation;
3212                }
3213
3214                @Override
3215                @NonNull
3216                public Boolean isCanceled() {
3217                        synchronized (this.lock) {
3218                                boolean canceled = this.canceled || this.reservation != null && this.reservation.isCanceled();
3219                                return !isCompleted() && canceled;
3220                        }
3221                }
3222
3223                @Override
3224                @NonNull
3225                public Optional<@NonNull StreamTerminationReason> getCancelationReason() {
3226                        synchronized (this.lock) {
3227                                Optional<StreamTerminationReason> reason = this.reason == null && this.reservation != null
3228                                                ? this.reservation.reason() : Optional.ofNullable(this.reason);
3229                                return isCompleted() ? Optional.empty() : reason;
3230                        }
3231                }
3232
3233                @Override
3234                @NonNull
3235                public Optional<@NonNull Throwable> getCancelationCause() {
3236                        synchronized (this.lock) {
3237                                Optional<Throwable> cause = this.reason == null && this.reservation != null
3238                                                ? this.reservation.cause() : Optional.ofNullable(this.cause);
3239                                return isCompleted() ? Optional.empty() : cause;
3240                        }
3241                }
3242
3243                @Override
3244                @NonNull
3245                public CallbackRegistration onCancel(@NonNull Runnable callback) {
3246                        requireNonNull(callback);
3247                        CancelationCallbackRegistration registration = new CancelationCallbackRegistration(callback);
3248                        boolean runImmediately;
3249
3250                        synchronized (this.lock) {
3251                                if (isCompleted()) {
3252                                        registration.callback = null;
3253                                        return registration;
3254                                }
3255                                runImmediately = this.canceled;
3256                                if (!runImmediately) {
3257                                        if (this.callbacks == null)
3258                                                this.callbacks = new LinkedHashSet<>();
3259                                        this.callbacks.add(registration);
3260                                }
3261                        }
3262                        if (runImmediately)
3263                                registration.invoke();
3264                        return registration;
3265                }
3266
3267                private boolean cancel(@NonNull StreamTerminationReason reason,
3268                                @Nullable Throwable cause) {
3269                        requireNonNull(reason);
3270                        if (reason == StreamTerminationReason.COMPLETED)
3271                                throw new IllegalArgumentException("Cancelation reason cannot be COMPLETED");
3272                        if (this.reservation != null)
3273                                return this.reservation.cancel(reason, cause);
3274                        Runnable callbacks = reserveCancelation(reason, cause);
3275                        if (callbacks == null)
3276                                return false;
3277                        callbacks.run();
3278                        return true;
3279                }
3280
3281                private void deliverCancelation(@NonNull StreamTerminationReason reason, @Nullable Throwable cause) {
3282                        Runnable callbacks = reserveCancelation(reason, cause);
3283                        if (callbacks != null && !sameInstance(callbacks, NO_CALLBACKS))
3284                                requireNonNull(this.reservation).dispatchCallbacks(callbacks);
3285                }
3286
3287                @Nullable
3288                private Runnable reserveCancelation(@NonNull StreamTerminationReason reason, @Nullable Throwable cause) {
3289
3290                        Set<CancelationCallbackRegistration> callbacksToRun;
3291                        synchronized (this.lock) {
3292                                if (this.canceled || isCompleted())
3293                                        return null;
3294                                this.reason = reason;
3295                                this.cause = cause;
3296                                this.canceled = true;
3297                                callbacksToRun = this.callbacks;
3298                                this.callbacks = null;
3299                        }
3300                        if (callbacksToRun == null || callbacksToRun.isEmpty())
3301                                return NO_CALLBACKS;
3302                        return () -> {
3303                                for (CancelationCallbackRegistration registration : callbacksToRun)
3304                                        registration.invoke();
3305                                callbacksToRun.clear();
3306                        };
3307                }
3308
3309                private void complete() {
3310                        Set<CancelationCallbackRegistration> completedCallbacks;
3311                        synchronized (this.lock) {
3312                                if (this.canceled || this.completed || this.reservation != null
3313                                                && !this.reservation.isProductionComplete() && this.reservation.isCanceled())
3314                                        return;
3315                                this.completed = true;
3316                                completedCallbacks = this.callbacks;
3317                                this.callbacks = null;
3318                        }
3319                        // Completion may release many application registrations. Keep traversal
3320                        // outside the token monitor used by framework cancellation signals.
3321                        if (completedCallbacks != null) {
3322                                for (CancelationCallbackRegistration registration : completedCallbacks)
3323                                        registration.callback = null;
3324                                completedCallbacks.clear();
3325                        }
3326                }
3327
3328                private boolean isCompleted() {
3329                        synchronized (this.lock) {
3330                                return this.completed || this.reservation != null && this.reservation.isProductionComplete();
3331                        }
3332                }
3333
3334                private void runCallback(@NonNull Runnable callback) {
3335                        try {
3336                                callback.run();
3337                        } catch (Throwable throwable) {
3338                                try {
3339                                        this.callbackFailureConsumer.accept(throwable);
3340                                } catch (Throwable ignored) {
3341                                        // Diagnostic observers cannot suppress remaining callbacks.
3342                                }
3343                        }
3344                }
3345
3346                private final class CancelationCallbackRegistration implements CallbackRegistration {
3347                        @Nullable
3348                        private volatile Runnable callback;
3349
3350                        private CancelationCallbackRegistration(@NonNull Runnable callback) {
3351                                this.callback = callback;
3352                        }
3353
3354                        @Override
3355                        public void close() {
3356                                synchronized (SimulatorCancelationToken.this.lock) {
3357                                        this.callback = null;
3358                                        if (SimulatorCancelationToken.this.callbacks != null)
3359                                                SimulatorCancelationToken.this.callbacks.remove(this);
3360                                }
3361                        }
3362
3363                        private void invoke() {
3364                                Runnable callback;
3365                                synchronized (SimulatorCancelationToken.this.lock) {
3366                                        callback = this.callback;
3367                                        this.callback = null;
3368                                }
3369                                if (callback != null)
3370                                        runCallback(callback);
3371                        }
3372                }
3373        }
3374
3375        /**
3376         * Mock server that doesn't touch the network at all, useful for testing.
3377         *
3378         * @author <a href="https://www.revetkn.com">Mark Allen</a>
3379         */
3380        @ThreadSafe
3381        static class MockHttpServer implements HttpServer {
3382                @NonNull
3383                private final TransportIdentity transportIdentity = TransportIdentity.create();
3384                private final int streamingLifecycleCapacity;
3385                private final int streamingCallbackConcurrency;
3386                @NonNull
3387                private final Duration streamingCleanupTimeout;
3388                @Nullable
3389                private volatile SokletConfig sokletConfig;
3390                private volatile HttpServer.@Nullable RequestHandler requestHandler;
3391
3392                MockHttpServer() {
3393                        this(null);
3394                }
3395
3396                MockHttpServer(@Nullable HttpServer sourceHttpServer) {
3397                        if (sourceHttpServer instanceof DefaultHttpServer defaultHttpServer) {
3398                                this.streamingLifecycleCapacity = defaultHttpServer.getStreamingLifecycleCapacity();
3399                                this.streamingCallbackConcurrency = defaultHttpServer.getStreamingCallbackConcurrency();
3400                                this.streamingCleanupTimeout = defaultHttpServer.getStreamingCleanupTimeout();
3401                        } else if (sourceHttpServer instanceof MockHttpServer mockHttpServer) {
3402                                this.streamingLifecycleCapacity = mockHttpServer.streamingLifecycleCapacity;
3403                                this.streamingCallbackConcurrency = mockHttpServer.streamingCallbackConcurrency;
3404                                this.streamingCleanupTimeout = mockHttpServer.streamingCleanupTimeout;
3405                        } else {
3406                                this.streamingLifecycleCapacity = DefaultHttpServer.DEFAULT_STREAMING_LIFECYCLE_CAPACITY;
3407                                this.streamingCallbackConcurrency = DefaultHttpServer.DEFAULT_STREAMING_CALLBACK_CONCURRENCY;
3408                                this.streamingCleanupTimeout = DefaultHttpServer.DEFAULT_STREAMING_CLEANUP_TIMEOUT;
3409                        }
3410                }
3411
3412                @NonNull
3413                @Override
3414                public TransportIdentity getTransportIdentity() {
3415                        return this.transportIdentity;
3416                }
3417
3418                @NonNull
3419                @Override
3420                public TransportRuntime attach(
3421                                @NonNull HttpTransportAttachmentContext attachmentContext,
3422                                @NonNull StartupContext startupContext) {
3423                        requireNonNull(startupContext);
3424                        HttpTransportAttachmentContext exactContext = requireNonNull(
3425                                        attachmentContext);
3426                        initialize(exactContext.getSokletConfig(),
3427                                        exactContext.getAdmissionFencedRequestHandler());
3428                        TransportTerminationSignal signal = exactContext.getTransportTerminationSignal();
3429                        return new TransportRuntime() {
3430                                @Override
3431                                public void start(@NonNull StartupContext context) {
3432                                        requireNonNull(context);
3433                                }
3434
3435                                @Override
3436                                public void shutdownGracefully(@NonNull ShutdownContext context) {
3437                                        requireNonNull(context);
3438                                        signal.signalTerminated();
3439                                }
3440
3441                                @Override
3442                                public void shutdownForcibly(@NonNull ShutdownContext context) {
3443                                        requireNonNull(context);
3444                                        signal.signalTerminated();
3445                                }
3446                        };
3447                }
3448
3449                void start() {
3450                        // No-op
3451                }
3452
3453                public void stop() {
3454                        // No-op
3455                }
3456
3457                @NonNull
3458                public Boolean isStarted() {
3459                        return true;
3460                }
3461
3462                public synchronized void initialize(@NonNull SokletConfig sokletConfig,
3463                                @NonNull RequestHandler requestHandler) {
3464                        requireNonNull(sokletConfig);
3465                        requireNonNull(requestHandler);
3466
3467                        this.sokletConfig = sokletConfig;
3468                        this.requestHandler = requestHandler;
3469                }
3470
3471                synchronized void releaseSimulationScopeState() {
3472                        this.sokletConfig = null;
3473                        this.requestHandler = null;
3474                }
3475
3476                @NonNull
3477                synchronized Optional<@NonNull SokletConfig> getSokletConfig() {
3478                        return Optional.ofNullable(this.sokletConfig);
3479                }
3480
3481                @NonNull
3482                synchronized Optional<@NonNull RequestHandler> getRequestHandler() {
3483                        return Optional.ofNullable(this.requestHandler);
3484                }
3485        }
3486
3487
3488        /**
3489         * Mock Server-Sent Event unicaster that doesn't touch the network at all, useful for testing.
3490         */
3491        @ThreadSafe
3492        static class MockSseUnicaster implements SseUnicaster {
3493                private final @NonNull Request request;
3494                private final @NonNull Consumer<@NonNull SseEvent> eventConsumer;
3495                private final @NonNull Consumer<@NonNull SseComment> commentConsumer;
3496                private final @NonNull Object initializerLock = new Object();
3497                private boolean initializing;
3498
3499                MockSseUnicaster(@NonNull Request request,
3500                                @NonNull Consumer<@NonNull SseEvent> eventConsumer, @NonNull Consumer<@NonNull SseComment> commentConsumer) {
3501                        this.request = requireNonNull(request);
3502                        this.eventConsumer = requireNonNull(eventConsumer);
3503                        this.commentConsumer = requireNonNull(commentConsumer);
3504                }
3505
3506                void beginInitializer() {
3507                        synchronized (this.initializerLock) { this.initializing = true; }
3508                }
3509                void finishInitializer() {
3510                        synchronized (this.initializerLock) { this.initializing = false; }
3511                }
3512
3513                @Override public void unicastEvent(@NonNull SseEvent event) {
3514                        synchronized (this.initializerLock) {
3515                                if (!this.initializing)
3516                                        throw new IllegalStateException("The SSE unicaster is available only during client initialization");
3517                                this.eventConsumer.accept(requireNonNull(event));
3518                        }
3519                }
3520                @Override public void unicastComment(@NonNull SseComment comment) {
3521                        synchronized (this.initializerLock) {
3522                                if (!this.initializing)
3523                                        throw new IllegalStateException("The SSE unicaster is available only during client initialization");
3524                                this.commentConsumer.accept(requireNonNull(comment));
3525                        }
3526                }
3527                @Override public @NonNull ResourcePath getResourcePath() { return this.request.getResourcePath(); }
3528        }
3529
3530        /**
3531         * Mock Server-Sent Event broadcaster that doesn't touch the network at all, useful for testing.
3532         */
3533        @ThreadSafe
3534        static class MockSseBroadcaster implements SseBroadcaster {
3535                // ConcurrentHashMap doesn't allow null values, so we use a sentinel if context is null
3536                private static final @NonNull Object NULL_CONTEXT_SENTINEL;
3537
3538                static {
3539                        NULL_CONTEXT_SENTINEL = new Object();
3540                }
3541
3542                @NonNull
3543                private final ResourcePath resourcePath;
3544                // Maps the Consumer (Listener) to its Context object (e.g. Locale)
3545                @NonNull
3546                private final Map<@NonNull Consumer<@NonNull SseEvent>, @NonNull Object> eventConsumers;
3547                // Same goes for comments
3548                @NonNull
3549                private final Map<@NonNull Consumer<@NonNull SseComment>, @NonNull Object> commentConsumers;
3550                private final @NonNull Set<@NonNull HandshakeAccepted> connections = ConcurrentHashMap.newKeySet();
3551                @NonNull
3552                private final AtomicReference<@Nullable Consumer<@NonNull Throwable>> broadcastErrorHandler;
3553                @NonNull
3554                private final Consumer<@NonNull LogEvent> logEventConsumer;
3555
3556                public MockSseBroadcaster(@NonNull ResourcePath resourcePath,
3557                                                                                                                        @NonNull AtomicReference<@Nullable Consumer<@NonNull Throwable>> broadcastErrorHandler,
3558                                                                                                                        @NonNull Consumer<@NonNull LogEvent> logEventConsumer) {
3559                        requireNonNull(resourcePath);
3560                        requireNonNull(broadcastErrorHandler);
3561                        requireNonNull(logEventConsumer);
3562
3563                        this.resourcePath = resourcePath;
3564                        this.eventConsumers = new ConcurrentHashMap<>();
3565                        this.commentConsumers = new ConcurrentHashMap<>();
3566                        this.broadcastErrorHandler = broadcastErrorHandler;
3567                        this.logEventConsumer = logEventConsumer;
3568                }
3569
3570                @NonNull
3571                @Override
3572                public ResourcePath getResourcePath() {
3573                        return this.resourcePath;
3574                }
3575
3576                @NonNull
3577                @Override
3578                public synchronized Long getClientCount() {
3579                        return Long.valueOf(getEventConsumers().size() + getCommentConsumers().size() - this.connections.size());
3580                }
3581
3582                @Override
3583                public void broadcastEvent(@NonNull SseEvent sseEvent) {
3584                        requireNonNull(sseEvent);
3585
3586                        for (Consumer<SseEvent> eventConsumer : getEventConsumers().keySet()) {
3587                                try {
3588                                        eventConsumer.accept(sseEvent);
3589                                } catch (Throwable throwable) {
3590                                        handleBroadcastError(throwable);
3591                                }
3592                        }
3593                }
3594
3595                @Override
3596                public void broadcastComment(@NonNull SseComment sseComment) {
3597                        requireNonNull(sseComment);
3598
3599                        for (Consumer<SseComment> commentConsumer : getCommentConsumers().keySet()) {
3600                                try {
3601                                        commentConsumer.accept(sseComment);
3602                                } catch (Throwable throwable) {
3603                                        handleBroadcastError(throwable);
3604                                }
3605                        }
3606                }
3607
3608                @Override
3609                public <T> void broadcastEvent(@NonNull Function<@Nullable Object, @Nullable T> keySelector,
3610                                @NonNull Function<@Nullable T, @NonNull SseEvent> eventProvider) {
3611                        requireNonNull(keySelector);
3612                        requireNonNull(eventProvider);
3613                        Map<T, SseBroadcastPayload<SseEvent>> payloadCache = new HashMap<>();
3614
3615                        getEventConsumers().forEach((consumer, context) -> {
3616                                try {
3617                                        Object clientContext = sameInstance(context, NULL_CONTEXT_SENTINEL) ? null : context;
3618                                        T key = keySelector.apply(clientContext);
3619                                        SseBroadcastPayload<SseEvent> generation = payloadCache.computeIfAbsent(key,
3620                                                        cacheKey -> SseBroadcastPayload.fromSupplier(() -> eventProvider.apply(cacheKey)));
3621                                        SseEvent event = generation.getPayload();
3622                                        if (event == null) {
3623                                                generation.recordFailedClient();
3624                                                return;
3625                                        }
3626                                        // A failing consumer cannot invalidate the payload shared by other clients.
3627                                        consumer.accept(event);
3628                                } catch (Throwable throwable) {
3629                                        handleBroadcastError(throwable);
3630                                }
3631                        });
3632                        payloadCache.values().forEach(generation -> handleBroadcastGenerationFailure(generation, false));
3633                }
3634
3635                @Override
3636                public <T> void broadcastComment(@NonNull Function<@Nullable Object, @Nullable T> keySelector,
3637                                @NonNull Function<@Nullable T, @NonNull SseComment> commentProvider) {
3638                        requireNonNull(keySelector);
3639                        requireNonNull(commentProvider);
3640                        Map<T, SseBroadcastPayload<SseComment>> payloadCache = new HashMap<>();
3641
3642                        getCommentConsumers().forEach((consumer, context) -> {
3643                                try {
3644                                        Object clientContext = sameInstance(context, NULL_CONTEXT_SENTINEL) ? null : context;
3645                                        T key = keySelector.apply(clientContext);
3646                                        SseBroadcastPayload<SseComment> generation = payloadCache.computeIfAbsent(key,
3647                                                        cacheKey -> SseBroadcastPayload.fromSupplier(() -> commentProvider.apply(cacheKey)));
3648                                        SseComment comment = generation.getPayload();
3649                                        if (comment == null) {
3650                                                generation.recordFailedClient();
3651                                                return;
3652                                        }
3653                                        consumer.accept(comment);
3654                                } catch (Throwable throwable) {
3655                                        handleBroadcastError(throwable);
3656                                }
3657                        });
3658                        payloadCache.values().forEach(generation -> handleBroadcastGenerationFailure(generation, true));
3659                }
3660
3661                private void handleBroadcastGenerationFailure(@NonNull SseBroadcastPayload<?> generation, boolean comment) {
3662                        generation.getFailure().ifPresent(failure -> handleBroadcastError(failure,
3663                                        LogEventType.SSE_SERVER_BROADCAST_GENERATION_FAILED,
3664                                        format("Failed to generate %s for %d connections on %s",
3665                                                        comment ? "Server-Sent Event comment" : "Server-Sent Event",
3666                                                        generation.getFailedClientCount(), getResourcePath())));
3667                }
3668
3669                @NonNull
3670                public Boolean registerEventConsumer(@NonNull Consumer<@NonNull SseEvent> eventConsumer) {
3671                        return registerEventConsumer(eventConsumer, null);
3672                }
3673
3674                /**
3675                 * Registers a consumer with an associated context, simulating a client with specific traits.
3676                 */
3677                @NonNull
3678                public Boolean registerEventConsumer(@NonNull Consumer<@NonNull SseEvent> eventConsumer, @Nullable Object context) {
3679                        requireNonNull(eventConsumer);
3680                        // map.put returns null if the key was new, which conceptually matches "add" returning true
3681                        return this.getEventConsumers().put(eventConsumer, context == null ? NULL_CONTEXT_SENTINEL : context) == null;
3682                }
3683
3684                @NonNull
3685                public Boolean unregisterEventConsumer(@NonNull Consumer<@NonNull SseEvent> eventConsumer) {
3686                        requireNonNull(eventConsumer);
3687                        return this.getEventConsumers().remove(eventConsumer) != null;
3688                }
3689
3690                @NonNull
3691                public Boolean registerCommentConsumer(@NonNull Consumer<@NonNull SseComment> commentConsumer) {
3692                        return registerCommentConsumer(commentConsumer, null);
3693                }
3694
3695                /**
3696                 * Registers a consumer with an associated context, simulating a client with specific traits.
3697                 */
3698                @NonNull
3699                public Boolean registerCommentConsumer(@NonNull Consumer<@NonNull SseComment> commentConsumer, @Nullable Object context) {
3700                        requireNonNull(commentConsumer);
3701                        return this.getCommentConsumers().put(commentConsumer, context == null ? NULL_CONTEXT_SENTINEL : context) == null;
3702                }
3703
3704                @NonNull
3705                public Boolean unregisterCommentConsumer(@NonNull Consumer<@NonNull SseComment> commentConsumer) {
3706                        requireNonNull(commentConsumer);
3707                        return this.getCommentConsumers().remove(commentConsumer) != null;
3708                }
3709
3710                @NonNull
3711                Map<@NonNull Consumer<@NonNull SseEvent>, @NonNull Object> getEventConsumers() {
3712                        return this.eventConsumers;
3713                }
3714
3715                @NonNull
3716                Map<@NonNull Consumer<@NonNull SseComment>, @NonNull Object> getCommentConsumers() {
3717                        return this.commentConsumers;
3718                }
3719
3720                synchronized void registerConnection(@NonNull HandshakeAccepted connection, @NonNull Consumer<@NonNull SseEvent> events,
3721                                @NonNull Consumer<@NonNull SseComment> comments, @Nullable Object context) {
3722                        this.connections.add(connection);
3723                        registerEventConsumer(events, context);
3724                        registerCommentConsumer(comments, context);
3725                }
3726
3727                synchronized void unregisterConnection(@NonNull HandshakeAccepted connection, @NonNull Consumer<@NonNull SseEvent> events,
3728                                @NonNull Consumer<@NonNull SseComment> comments) {
3729                        unregisterEventConsumer(events);
3730                        unregisterCommentConsumer(comments);
3731                        this.connections.remove(connection);
3732                }
3733
3734                synchronized void releaseSimulationScopeState() {
3735                        this.connections.clear();
3736                        this.eventConsumers.clear();
3737                        this.commentConsumers.clear();
3738                }
3739
3740                void handleBroadcastError(@NonNull Throwable throwable) {
3741                        handleBroadcastError(throwable, LogEventType.SSE_SERVER_INTERNAL_ERROR,
3742                                        "SSE simulator broadcast consumer failed");
3743                }
3744
3745                private void handleBroadcastError(@NonNull Throwable throwable,
3746                                @NonNull LogEventType logEventType, @NonNull String message) {
3747                        requireNonNull(throwable);
3748                        Consumer<Throwable> handler = this.broadcastErrorHandler.get();
3749                        if (handler != null) {
3750                                try {
3751                                        handler.accept(throwable);
3752                                        return;
3753                                } catch (Throwable ignored) {
3754                                        // Fall through to default behavior.
3755                                }
3756                        }
3757                        safelyLog(LogEvent.with(logEventType, message).throwable(throwable).build());
3758                }
3759
3760                void safelyLog(@NonNull LogEvent logEvent) {
3761                        requireNonNull(logEvent);
3762
3763                        try {
3764                                this.logEventConsumer.accept(logEvent);
3765                        } catch (Throwable ignored) {
3766                                // No safe fallback sink is available here.
3767                        }
3768                }
3769        }
3770
3771        /**
3772         * Mock Server-Sent Event server that doesn't touch the network at all, useful for testing.
3773         *
3774         * @author <a href="https://www.revetkn.com">Mark Allen</a>
3775         */
3776        @ThreadSafe
3777        static class MockSseServer implements SseServer {
3778                @NonNull
3779                private final TransportIdentity transportIdentity;
3780                final int streamingLifecycleCapacity;
3781                final int connectionQueueCapacity;
3782                @Nullable
3783                private volatile SokletConfig sokletConfig;
3784                private volatile SseServer.@Nullable RequestHandler requestHandler;
3785                @NonNull
3786                private volatile Set<@NonNull ResourcePathDeclaration>
3787                                resourcePathDeclarations;
3788                private volatile boolean started;
3789                @NonNull
3790                private final ConcurrentHashMap<@NonNull ResourcePath, @NonNull MockSseBroadcaster> broadcastersByResourcePath;
3791                @NonNull
3792                private final AtomicReference<@Nullable Consumer<@NonNull Throwable>> broadcastErrorHandler;
3793                @NonNull
3794                private final AtomicReference<@Nullable Consumer<@NonNull Throwable>> unicastErrorHandler;
3795
3796                public MockSseServer() { this(null); }
3797
3798                MockSseServer(@Nullable SseServer sourceSseServer) {
3799                        if (sourceSseServer instanceof DefaultSseServer server) {
3800                                this.streamingLifecycleCapacity = server.getStreamingLifecycleCapacity();
3801                                this.connectionQueueCapacity = server.getConnectionQueueCapacity();
3802                        } else if (sourceSseServer instanceof MockSseServer server) {
3803                                this.streamingLifecycleCapacity = server.streamingLifecycleCapacity;
3804                                this.connectionQueueCapacity = server.connectionQueueCapacity;
3805                        } else {
3806                                this.streamingLifecycleCapacity = DefaultSseServer.DEFAULT_STREAMING_LIFECYCLE_CAPACITY;
3807                                this.connectionQueueCapacity = 128;
3808                        }
3809                        this.transportIdentity = TransportIdentity.create();
3810                        this.broadcastersByResourcePath = new ConcurrentHashMap<>();
3811                        this.broadcastErrorHandler = new AtomicReference<>();
3812                        this.unicastErrorHandler = new AtomicReference<>();
3813                        this.resourcePathDeclarations = Set.of();
3814                }
3815
3816                @NonNull
3817                @Override
3818                public TransportIdentity getTransportIdentity() {
3819                        return this.transportIdentity;
3820                }
3821
3822                @NonNull
3823                @Override
3824                public TransportRuntime attach(
3825                                @NonNull SseTransportAttachmentContext attachmentContext,
3826                                @NonNull StartupContext startupContext) {
3827                        requireNonNull(startupContext);
3828                        SseTransportAttachmentContext exactContext = requireNonNull(
3829                                        attachmentContext);
3830                        initialize(exactContext.getSokletConfig(),
3831                                        exactContext.getAdmissionFencedRequestHandler());
3832                        TransportTerminationSignal signal = exactContext.getTransportTerminationSignal();
3833                        return new TransportRuntime() {
3834                                @Override
3835                                public void start(@NonNull StartupContext context) {
3836                                        requireNonNull(context);
3837                                        MockSseServer.this.started = true;
3838                                }
3839
3840                                @Override
3841                                public void shutdownGracefully(@NonNull ShutdownContext context) {
3842                                        requireNonNull(context);
3843                                        MockSseServer.this.started = false;
3844                                        signal.signalTerminated();
3845                                }
3846
3847                                @Override
3848                                public void shutdownForcibly(@NonNull ShutdownContext context) {
3849                                        requireNonNull(context);
3850                                        MockSseServer.this.started = false;
3851                                        signal.signalTerminated();
3852                                }
3853                        };
3854                }
3855
3856                void start() {
3857                        this.started = true;
3858                }
3859
3860                public void stop() {
3861                        this.started = false;
3862                }
3863
3864                @NonNull
3865                public Boolean isStarted() {
3866                        return this.started;
3867                }
3868
3869                @NonNull
3870                @Override
3871                public synchronized Optional<? extends @NonNull SseBroadcaster> acquireBroadcaster(
3872                                @Nullable ResourcePath resourcePath) {
3873                        if (resourcePath == null || !this.started
3874                                        || this.sokletConfig == null)
3875                                return Optional.empty();
3876                        if (this.resourcePathDeclarations.stream().noneMatch(
3877                                        declaration -> declaration.matches(resourcePath)))
3878                                return Optional.empty();
3879
3880                        MockSseBroadcaster broadcaster = getBroadcastersByResourcePath()
3881                                        .computeIfAbsent(resourcePath, rp -> new MockSseBroadcaster(rp, broadcastErrorHandler, this::safelyLog));
3882
3883                        return Optional.of(broadcaster);
3884                }
3885
3886                void registerConnection(@NonNull HandshakeAccepted connection, @NonNull ResourcePath resourcePath,
3887                                @NonNull Consumer<@NonNull SseEvent> events, @NonNull Consumer<@NonNull SseComment> comments, @Nullable Object context) {
3888                        this.broadcastersByResourcePath.computeIfAbsent(resourcePath,
3889                                        rp -> new MockSseBroadcaster(rp, this.broadcastErrorHandler, this::safelyLog))
3890                                        .registerConnection(connection, events, comments, context);
3891                }
3892
3893                void unregisterConnection(@NonNull HandshakeAccepted connection, @NonNull ResourcePath resourcePath,
3894                                @NonNull Consumer<@NonNull SseEvent> events, @NonNull Consumer<@NonNull SseComment> comments) {
3895                        MockSseBroadcaster broadcaster = this.broadcastersByResourcePath.get(resourcePath);
3896                        if (broadcaster != null)
3897                                broadcaster.unregisterConnection(connection, events, comments);
3898                }
3899
3900                public void registerEventConsumer(@NonNull ResourcePath resourcePath,
3901                                                                                                                                                        @NonNull Consumer<@NonNull SseEvent> eventConsumer) {
3902                        registerEventConsumer(resourcePath, eventConsumer, null);
3903                }
3904
3905                public void registerEventConsumer(@NonNull ResourcePath resourcePath,
3906                                                                                                                                                        @NonNull Consumer<@NonNull SseEvent> eventConsumer,
3907                                                                                                                                                        @Nullable Object context) {
3908                        requireNonNull(resourcePath);
3909                        requireNonNull(eventConsumer);
3910
3911                        MockSseBroadcaster broadcaster = getBroadcastersByResourcePath()
3912                                        .computeIfAbsent(resourcePath, rp -> new MockSseBroadcaster(rp, broadcastErrorHandler, this::safelyLog));
3913
3914                        broadcaster.registerEventConsumer(eventConsumer, context);
3915                }
3916
3917                @NonNull
3918                public Boolean unregisterEventConsumer(@NonNull ResourcePath resourcePath,
3919                                                                                                                                                                         @NonNull Consumer<@NonNull SseEvent> eventConsumer) {
3920                        requireNonNull(resourcePath);
3921                        requireNonNull(eventConsumer);
3922
3923                        MockSseBroadcaster broadcaster = getBroadcastersByResourcePath().get(resourcePath);
3924
3925                        if (broadcaster == null)
3926                                return false;
3927
3928                        return broadcaster.unregisterEventConsumer(eventConsumer);
3929                }
3930
3931                public void registerCommentConsumer(@NonNull ResourcePath resourcePath,
3932                                                                                                                                                                @NonNull Consumer<@NonNull SseComment> commentConsumer) {
3933                        registerCommentConsumer(resourcePath, commentConsumer, null);
3934                }
3935
3936                public void registerCommentConsumer(@NonNull ResourcePath resourcePath,
3937                                                                                                                                                                @NonNull Consumer<@NonNull SseComment> commentConsumer,
3938                                                                                                                                                                @Nullable Object context) {
3939                        requireNonNull(resourcePath);
3940                        requireNonNull(commentConsumer);
3941
3942                        MockSseBroadcaster broadcaster = getBroadcastersByResourcePath()
3943                                        .computeIfAbsent(resourcePath, rp -> new MockSseBroadcaster(rp, broadcastErrorHandler, this::safelyLog));
3944
3945                        broadcaster.registerCommentConsumer(commentConsumer, context);
3946                }
3947
3948                @NonNull
3949                public Boolean unregisterCommentConsumer(@NonNull ResourcePath resourcePath,
3950                                                                                                                                                                                 @NonNull Consumer<@NonNull SseComment> commentConsumer) {
3951                        requireNonNull(resourcePath);
3952                        requireNonNull(commentConsumer);
3953
3954                        MockSseBroadcaster broadcaster = getBroadcastersByResourcePath().get(resourcePath);
3955
3956                        if (broadcaster == null)
3957                                return false;
3958
3959                        return broadcaster.unregisterCommentConsumer(commentConsumer);
3960                }
3961
3962                public synchronized void initialize(@NonNull SokletConfig sokletConfig,
3963                                SseServer.@NonNull RequestHandler requestHandler) {
3964                        requireNonNull(sokletConfig);
3965                        requireNonNull(requestHandler);
3966
3967                        this.sokletConfig = sokletConfig;
3968                        this.requestHandler = requestHandler;
3969                        this.resourcePathDeclarations = Set.copyOf(sokletConfig
3970                                        .getResourceMethodResolver().getResourceMethods().stream()
3971                                        .filter(ResourceMethod::isSseEventSource)
3972                                        .map(ResourceMethod::getResourcePathDeclaration)
3973                                        .toList());
3974                }
3975
3976                synchronized void releaseSimulationScopeState() {
3977                        this.started = false;
3978                        this.sokletConfig = null;
3979                        this.requestHandler = null;
3980                        this.resourcePathDeclarations = Set.of();
3981                        for (MockSseBroadcaster broadcaster
3982                                        : this.broadcastersByResourcePath.values())
3983                                broadcaster.releaseSimulationScopeState();
3984                        this.broadcastersByResourcePath.clear();
3985                        this.broadcastErrorHandler.set(null);
3986                        this.unicastErrorHandler.set(null);
3987                }
3988
3989                public void onBroadcastError(@Nullable Consumer<@NonNull Throwable> onBroadcastError) {
3990                        this.broadcastErrorHandler.set(onBroadcastError);
3991                }
3992
3993                public void onUnicastError(@Nullable Consumer<@NonNull Throwable> onUnicastError) {
3994                        this.unicastErrorHandler.set(onUnicastError);
3995                }
3996
3997                void notifyConnectionLifecycle(@NonNull Request request, @Nullable ResourceMethod resourceMethod,
3998                                @NonNull LogEventType failureType, @NonNull String callbackName,
3999                                @NonNull Consumer<@NonNull LifecycleObserver> observerNotification,
4000                                @NonNull Consumer<@NonNull MetricsCollector> metricsNotification) {
4001                        SokletConfig config = this.sokletConfig;
4002                        if (config == null)
4003                                return;
4004                        try {
4005                                observerNotification.accept(config.getAggregateLifecycleObserver());
4006                        } catch (Throwable failure) {
4007                                safelyLog(LogEvent.with(failureType, "An exception occurred while invoking LifecycleObserver::" + callbackName)
4008                                                .request(request).resourceMethod(resourceMethod).throwable(failure).build());
4009                        }
4010                        try {
4011                                metricsNotification.accept(config.getMetricsCollector());
4012                        } catch (Throwable failure) {
4013                                safelyLog(LogEvent.with(LogEventType.METRICS_COLLECTOR_FAILED,
4014                                                "An exception occurred while invoking MetricsCollector::" + callbackName)
4015                                                .request(request).resourceMethod(resourceMethod).throwable(failure).build());
4016                        }
4017                }
4018
4019                void safelyLog(@NonNull LogEvent logEvent) {
4020                        requireNonNull(logEvent);
4021
4022                        SokletConfig sokletConfig = this.sokletConfig;
4023
4024                        if (sokletConfig == null)
4025                                return;
4026
4027                        try {
4028                                sokletConfig.getAggregateLifecycleObserver().didReceiveLogEvent(logEvent);
4029                        } catch (Throwable observerFailure) {
4030                                LifecycleObserverLogFallback.report(observerFailure);
4031                        }
4032                }
4033
4034                @NonNull
4035                synchronized Optional<@NonNull SokletConfig> getSokletConfig() {
4036                        return Optional.ofNullable(this.sokletConfig);
4037                }
4038
4039                @NonNull
4040                synchronized Optional<SseServer.@NonNull RequestHandler> getRequestHandler() {
4041                        return Optional.ofNullable(this.requestHandler);
4042                }
4043
4044                @NonNull
4045                ConcurrentHashMap<@NonNull ResourcePath, @NonNull MockSseBroadcaster> getBroadcastersByResourcePath() {
4046                        return this.broadcastersByResourcePath;
4047                }
4048
4049                @NonNull
4050                AtomicReference<@Nullable Consumer<@NonNull Throwable>> getBroadcastErrorHandler() {
4051                        return this.broadcastErrorHandler;
4052                }
4053
4054                @NonNull
4055                AtomicReference<@Nullable Consumer<@NonNull Throwable>> getUnicastErrorHandler() {
4056                        return this.unicastErrorHandler;
4057                }
4058        }
4059
4060}