001/*
002 * Copyright 2022-2025 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.core.impl;
018
019import com.soklet.SokletConfiguration;
020import com.soklet.core.HttpMethod;
021import com.soklet.core.LifecycleInterceptor;
022import com.soklet.core.LogEvent;
023import com.soklet.core.LogEventType;
024import com.soklet.core.MarshaledResponse;
025import com.soklet.core.MultipartParser;
026import com.soklet.core.Request;
027import com.soklet.core.ResponseCookie;
028import com.soklet.core.Server;
029import com.soklet.core.StatusCode;
030import com.soklet.core.Utilities;
031import com.soklet.internal.microhttp.EventLoop;
032import com.soklet.internal.microhttp.Handler;
033import com.soklet.internal.microhttp.Header;
034import com.soklet.internal.microhttp.LogEntry;
035import com.soklet.internal.microhttp.Logger;
036import com.soklet.internal.microhttp.MicrohttpRequest;
037import com.soklet.internal.microhttp.MicrohttpResponse;
038import com.soklet.internal.microhttp.Options;
039import com.soklet.internal.microhttp.OptionsBuilder;
040import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
041
042import javax.annotation.Nonnull;
043import javax.annotation.Nullable;
044import javax.annotation.concurrent.NotThreadSafe;
045import javax.annotation.concurrent.ThreadSafe;
046import java.io.IOException;
047import java.io.UncheckedIOException;
048import java.nio.charset.Charset;
049import java.nio.charset.StandardCharsets;
050import java.time.Duration;
051import java.util.ArrayList;
052import java.util.Comparator;
053import java.util.LinkedHashSet;
054import java.util.List;
055import java.util.Map;
056import java.util.Optional;
057import java.util.Set;
058import java.util.SortedSet;
059import java.util.TreeSet;
060import java.util.concurrent.ExecutorService;
061import java.util.concurrent.Executors;
062import java.util.concurrent.ThreadFactory;
063import java.util.concurrent.TimeUnit;
064import java.util.concurrent.atomic.AtomicBoolean;
065import java.util.concurrent.atomic.AtomicInteger;
066import java.util.concurrent.locks.ReentrantLock;
067import java.util.function.Supplier;
068
069import static com.soklet.core.Utilities.emptyByteArray;
070import static com.soklet.core.Utilities.trimAggressivelyToNull;
071import static java.lang.String.format;
072import static java.util.Locale.ENGLISH;
073import static java.util.Objects.requireNonNull;
074
075/**
076 * @author <a href="https://www.revetkn.com">Mark Allen</a>
077 */
078@ThreadSafe
079public class DefaultServer implements Server {
080        @Nonnull
081        private static final String DEFAULT_HOST;
082        @Nonnull
083        private static final Integer DEFAULT_CONCURRENCY;
084        @Nonnull
085        private static final Duration DEFAULT_REQUEST_TIMEOUT;
086        @Nonnull
087        private static final Duration DEFAULT_SOCKET_SELECT_TIMEOUT;
088        @Nonnull
089        private static final Integer DEFAULT_MAXIMUM_REQUEST_SIZE_IN_BYTES;
090        @Nonnull
091        private static final Integer DEFAULT_REQUEST_READ_BUFFER_SIZE_IN_BYTES;
092        @Nonnull
093        private static final Integer DEFAULT_SOCKET_PENDING_CONNECTION_LIMIT;
094        @Nonnull
095        private static final Duration DEFAULT_SHUTDOWN_TIMEOUT;
096
097        static {
098                DEFAULT_HOST = "0.0.0.0";
099                DEFAULT_CONCURRENCY = Runtime.getRuntime().availableProcessors();
100                DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(60);
101                DEFAULT_SOCKET_SELECT_TIMEOUT = Duration.ofMillis(100);
102                DEFAULT_MAXIMUM_REQUEST_SIZE_IN_BYTES = 1_024 * 1_024 * 10;
103                DEFAULT_REQUEST_READ_BUFFER_SIZE_IN_BYTES = 1_024 * 64;
104                DEFAULT_SOCKET_PENDING_CONNECTION_LIMIT = 0;
105                DEFAULT_SHUTDOWN_TIMEOUT = Duration.ofSeconds(5);
106        }
107
108        @Nonnull
109        private final Integer port;
110        @Nonnull
111        private final String host;
112        @Nonnull
113        private final Integer concurrency;
114        @Nonnull
115        private final Duration requestTimeout;
116        @Nonnull
117        private final Duration socketSelectTimeout;
118        @Nonnull
119        private final Duration shutdownTimeout;
120        @Nonnull
121        private final Integer maximumRequestSizeInBytes;
122        @Nonnull
123        private final Integer requestReadBufferSizeInBytes;
124        @Nonnull
125        private final Integer socketPendingConnectionLimit;
126        @Nonnull
127        private final MultipartParser multipartParser;
128        @Nonnull
129        private final ReentrantLock lock;
130        @Nonnull
131        private final Supplier<ExecutorService> requestHandlerExecutorServiceSupplier;
132        @Nullable
133        private volatile ExecutorService requestHandlerExecutorService;
134        @Nullable
135        private volatile RequestHandler requestHandler;
136        @Nullable
137        private volatile LifecycleInterceptor lifecycleInterceptor;
138        @Nullable
139        private volatile EventLoop eventLoop;
140
141        @Nonnull
142        public static Builder withPort(@Nonnull Integer port) {
143                requireNonNull(port);
144                return new Builder(port);
145        }
146
147        protected DefaultServer(@Nonnull Builder builder) {
148                requireNonNull(builder);
149
150                this.lock = new ReentrantLock();
151
152                this.port = builder.port;
153                this.host = builder.host != null ? builder.host : DEFAULT_HOST;
154                this.concurrency = builder.concurrency != null ? builder.concurrency : DEFAULT_CONCURRENCY;
155                this.maximumRequestSizeInBytes = builder.maximumRequestSizeInBytes != null ? builder.maximumRequestSizeInBytes : DEFAULT_MAXIMUM_REQUEST_SIZE_IN_BYTES;
156                this.requestReadBufferSizeInBytes = builder.requestReadBufferSizeInBytes != null ? builder.requestReadBufferSizeInBytes : DEFAULT_REQUEST_READ_BUFFER_SIZE_IN_BYTES;
157                this.requestTimeout = builder.requestTimeout != null ? builder.requestTimeout : DEFAULT_REQUEST_TIMEOUT;
158                this.socketSelectTimeout = builder.socketSelectTimeout != null ? builder.socketSelectTimeout : DEFAULT_SOCKET_SELECT_TIMEOUT;
159                this.socketPendingConnectionLimit = builder.socketPendingConnectionLimit != null ? builder.socketPendingConnectionLimit : DEFAULT_SOCKET_PENDING_CONNECTION_LIMIT;
160                this.shutdownTimeout = builder.shutdownTimeout != null ? builder.shutdownTimeout : DEFAULT_SHUTDOWN_TIMEOUT;
161                this.multipartParser = builder.multipartParser != null ? builder.multipartParser : DefaultMultipartParser.sharedInstance();
162                this.requestHandlerExecutorServiceSupplier = builder.requestHandlerExecutorServiceSupplier != null ? builder.requestHandlerExecutorServiceSupplier : () -> {
163                        String threadNamePrefix = "request-handler-";
164
165                        if (Utilities.virtualThreadsAvailable())
166                                return Utilities.createVirtualThreadsNewThreadPerTaskExecutor(threadNamePrefix, (Thread thread, Throwable throwable) -> {
167                                        safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unexpected exception occurred during server HTTP request processing")
168                                                        .throwable(throwable)
169                                                        .build());
170                                });
171
172                        return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new NonvirtualThreadFactory(threadNamePrefix));
173                };
174        }
175
176        @Override
177        public void start() {
178                getLock().lock();
179
180                try {
181                        if (isStarted())
182                                return;
183
184                        Options options = OptionsBuilder.newBuilder()
185                                        .withHost(getHost())
186                                        .withPort(getPort())
187                                        .withConcurrency(getConcurrency())
188                                        .withRequestTimeout(getRequestTimeout())
189                                        .withResolution(getSocketSelectTimeout())
190                                        .withReadBufferSize(getRequestReadBufferSizeInBytes())
191                                        .withMaxRequestSize(getMaximumRequestSizeInBytes())
192                                        .withAcceptLength(getSocketPendingConnectionLimit())
193                                        .build();
194
195                        Logger logger = new Logger() {
196                                @Override
197                                public boolean enabled() {
198                                        return false;
199                                }
200
201                                @Override
202                                public void log(@Nullable LogEntry... logEntries) {
203                                        // No-op
204                                }
205
206                                @Override
207                                public void log(@Nullable Exception e,
208                                                                                                @Nullable LogEntry... logEntries) {
209                                        // No-op
210                                }
211                        };
212
213                        Handler handler = ((microhttpRequest, microHttpCallback) -> {
214                                ExecutorService requestHandlerExecutorServiceReference = this.requestHandlerExecutorService;
215
216                                if (requestHandlerExecutorServiceReference == null)
217                                        return;
218
219                                requestHandlerExecutorServiceReference.submit(() -> {
220                                        RequestHandler requestHandler = getRequestHandler().orElse(null);
221
222                                        if (requestHandler == null)
223                                                return;
224
225                                        AtomicBoolean shouldWriteFailsafeResponse = new AtomicBoolean(true);
226
227                                        try {
228                                                // Normalize body
229                                                byte[] body = microhttpRequest.body();
230
231                                                if (body != null && body.length == 0)
232                                                        body = null;
233
234                                                // Special case: look for a poison-pill header that indicates "content too large",
235                                                // make a note of it, and then remove it from the request.
236                                                // This header is specially set for Soklet inside of Microhttp's connection event loop.
237                                                Map<String, Set<String>> headers = headersFromMicrohttpRequest(microhttpRequest);
238                                                boolean contentTooLarge = false;
239
240                                                for (Header header : microhttpRequest.headers()) {
241                                                        if (header.name().equals("com.soklet.CONTENT_TOO_LARGE")) {
242                                                                headers.remove("com.soklet.CONTENT_TOO_LARGE");
243                                                                contentTooLarge = true;
244                                                        }
245                                                }
246
247                                                Request request = Request.with(HttpMethod.valueOf(microhttpRequest.method().toUpperCase(ENGLISH)), microhttpRequest.uri())
248                                                                .multipartParser(getMultipartParser())
249                                                                .headers(headers)
250                                                                .body(body)
251                                                                .contentTooLarge(contentTooLarge)
252                                                                .build();
253
254                                                requestHandler.handleRequest(request, (requestResult -> {
255                                                        try {
256                                                                MicrohttpResponse microhttpResponse = toMicrohttpResponse(requestResult.getMarshaledResponse());
257                                                                shouldWriteFailsafeResponse.set(false);
258
259                                                                try {
260                                                                        microHttpCallback.accept(microhttpResponse);
261                                                                } catch (Throwable t) {
262                                                                        safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unable to write response")
263                                                                                        .throwable(t)
264                                                                                        .build());
265                                                                }
266                                                        } catch (Throwable t) {
267                                                                safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An error occurred while marshaling to a response")
268                                                                                .throwable(t)
269                                                                                .build());
270
271                                                                try {
272                                                                        microHttpCallback.accept(provideMicrohttpFailsafeResponse(microhttpRequest, t));
273                                                                } catch (Throwable t2) {
274                                                                        safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An error occurred while writing a failsafe response")
275                                                                                        .throwable(t2)
276                                                                                        .build());
277                                                                }
278                                                        }
279                                                }));
280                                        } catch (Throwable t) {
281                                                safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An unexpected error occurred during request handling")
282                                                                .throwable(t)
283                                                                .build());
284
285                                                if (shouldWriteFailsafeResponse.get()) {
286                                                        try {
287                                                                microHttpCallback.accept(provideMicrohttpFailsafeResponse(microhttpRequest, t));
288                                                        } catch (Throwable t2) {
289                                                                safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An error occurred while writing a failsafe response")
290                                                                                .throwable(t2)
291                                                                                .build());
292                                                        }
293                                                }
294                                        }
295                                });
296                        });
297
298                        this.requestHandlerExecutorService = getRequestHandlerExecutorServiceSupplier().get();
299
300                        try {
301                                this.eventLoop = new EventLoop(options, logger, handler);
302                                eventLoop.start();
303                        } catch (IOException e) {
304                                throw new UncheckedIOException(e);
305                        }
306                } finally {
307                        getLock().unlock();
308                }
309        }
310
311        @Override
312        public void stop() {
313                getLock().lock();
314
315                try {
316                        if (!isStarted())
317                                return;
318
319                        try {
320                                getEventLoop().get().stop();
321                        } catch (Exception e) {
322                                safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unable to shut down server event loop")
323                                                .throwable(e)
324                                                .build());
325                        }
326
327                        boolean interrupted = false;
328
329                        try {
330                                getRequestHandlerExecutorService().get().shutdown();
331                                getRequestHandlerExecutorService().get().awaitTermination(getShutdownTimeout().getSeconds(), TimeUnit.SECONDS);
332                        } catch (InterruptedException e) {
333                                interrupted = true;
334                        } catch (Exception e) {
335                                safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unable to shut down server request handler executor service")
336                                                .throwable(e)
337                                                .build());
338                        } finally {
339                                if (interrupted)
340                                        Thread.currentThread().interrupt();
341                        }
342                } finally {
343                        this.eventLoop = null;
344                        this.requestHandlerExecutorService = null;
345
346                        getLock().unlock();
347                }
348        }
349
350        @Nonnull
351        protected MicrohttpResponse provideMicrohttpFailsafeResponse(@Nonnull MicrohttpRequest microhttpRequest,
352                                                                                                                                                                                                                                                         @Nonnull Throwable throwable) {
353                requireNonNull(microhttpRequest);
354                requireNonNull(throwable);
355
356                Integer statusCode = 500;
357                Charset charset = StandardCharsets.UTF_8;
358                String reasonPhrase = StatusCode.fromStatusCode(statusCode).get().getReasonPhrase();
359                List<Header> headers = List.of(new Header("Content-Type", format("text/plain; charset=%s", charset.name())));
360                byte[] body = format("HTTP %d: %s", statusCode, StatusCode.fromStatusCode(statusCode).get().getReasonPhrase()).getBytes(charset);
361
362                return new MicrohttpResponse(statusCode, reasonPhrase, headers, body);
363        }
364
365        @Nonnull
366        @Override
367        public Boolean isStarted() {
368                getLock().lock();
369
370                try {
371                        return getEventLoop().isPresent();
372                } finally {
373                        getLock().unlock();
374                }
375        }
376
377        @Override
378        public void initialize(@Nonnull SokletConfiguration sokletConfiguration,
379                                                                                                 @Nonnull RequestHandler requestHandler) {
380                requireNonNull(requestHandler);
381                requireNonNull(sokletConfiguration);
382
383                this.requestHandler = requestHandler;
384                this.lifecycleInterceptor = sokletConfiguration.getLifecycleInterceptor();
385        }
386
387        @Nonnull
388        protected Map<String, Set<String>> headersFromMicrohttpRequest(@Nonnull MicrohttpRequest microhttpRequest) {
389                requireNonNull(microhttpRequest);
390
391                Map<String, Set<String>> headers = new LinkedCaseInsensitiveMap<>(microhttpRequest.headers().size());
392
393                for (Header header : microhttpRequest.headers()) {
394                        Set<String> values = headers.computeIfAbsent(header.name(), k -> new LinkedHashSet<>());
395
396                        String value = trimAggressivelyToNull(header.value());
397
398                        if (value != null)
399                                values.add(value);
400                }
401
402                return headers;
403        }
404
405        @Nonnull
406        protected MicrohttpResponse toMicrohttpResponse(@Nonnull MarshaledResponse marshaledResponse) {
407                requireNonNull(marshaledResponse);
408
409                List<Header> headers = new ArrayList<>();
410
411                // Non-cookies headers get their values comma-separated
412                for (Map.Entry<String, Set<String>> entry : marshaledResponse.getHeaders().entrySet()) {
413                        String name = entry.getKey();
414                        Set<String> values = entry.getValue();
415
416                        // Force natural ordering for consistent output if the set is not already sorted.
417                        if (!isAlreadySorted(values))
418                                values = new TreeSet<>(entry.getValue());
419
420                        headers.add(new Header(name, String.join(", ", values)));
421                }
422
423                // ResponseCookie headers are split into multiple instances of Set-Cookie.
424                // Force natural ordering for consistent output if the set is not already sorted.
425                Set<ResponseCookie> cookies = marshaledResponse.getCookies();
426                List<ResponseCookie> sortedCookies = new ArrayList<>(cookies);
427
428                if (!isAlreadySorted(cookies))
429                        sortedCookies.sort(Comparator.comparing(ResponseCookie::getName));
430
431                for (ResponseCookie cookie : sortedCookies)
432                        headers.add(new Header("Set-Cookie", cookie.toSetCookieHeaderRepresentation()));
433
434                // Force natural order for consistent output
435                headers.sort(Comparator.comparing(Header::name));
436
437                String reasonPhrase = reasonPhraseForStatusCode(marshaledResponse.getStatusCode());
438                byte[] body = marshaledResponse.getBody().orElse(emptyByteArray());
439
440                return new MicrohttpResponse(marshaledResponse.getStatusCode(), reasonPhrase, headers, body);
441        }
442
443        @Nonnull
444        protected String reasonPhraseForStatusCode(@Nonnull Integer statusCode) {
445                requireNonNull(statusCode);
446
447                StatusCode formalStatusCode = StatusCode.fromStatusCode(statusCode).orElse(null);
448                return formalStatusCode == null ? "Unknown" : formalStatusCode.getReasonPhrase();
449        }
450
451        @Nonnull
452        protected Boolean isAlreadySorted(@Nonnull Set<?> set) {
453                requireNonNull(set);
454                return set instanceof SortedSet || set instanceof LinkedHashSet;
455        }
456
457        protected void safelyLog(@Nonnull LogEvent logEvent) {
458                requireNonNull(logEvent);
459
460                try {
461                        getLifecycleInterceptor().get().didReceiveLogEvent(logEvent);
462                } catch (Throwable throwable) {
463                        // The LifecycleInterceptor implementation errored out, but we can't let that affect us - swallow its exception.
464                        // Not much else we can do here but dump to stderr
465                        throwable.printStackTrace();
466                }
467        }
468
469        @Nonnull
470        protected Integer getPort() {
471                return this.port;
472        }
473
474        @Nonnull
475        protected Integer getConcurrency() {
476                return this.concurrency;
477        }
478
479        @Nonnull
480        protected String getHost() {
481                return this.host;
482        }
483
484        @Nonnull
485        protected Duration getRequestTimeout() {
486                return this.requestTimeout;
487        }
488
489        @Nonnull
490        protected Duration getSocketSelectTimeout() {
491                return this.socketSelectTimeout;
492        }
493
494        @Nonnull
495        protected Duration getShutdownTimeout() {
496                return this.shutdownTimeout;
497        }
498
499        @Nonnull
500        protected Integer getMaximumRequestSizeInBytes() {
501                return this.maximumRequestSizeInBytes;
502        }
503
504        @Nonnull
505        protected Integer getRequestReadBufferSizeInBytes() {
506                return this.requestReadBufferSizeInBytes;
507        }
508
509        @Nonnull
510        protected Integer getSocketPendingConnectionLimit() {
511                return this.socketPendingConnectionLimit;
512        }
513
514        @Nonnull
515        protected MultipartParser getMultipartParser() {
516                return this.multipartParser;
517        }
518
519        @Nonnull
520        protected Optional<ExecutorService> getRequestHandlerExecutorService() {
521                return Optional.ofNullable(this.requestHandlerExecutorService);
522        }
523
524        @Nonnull
525        protected ReentrantLock getLock() {
526                return this.lock;
527        }
528
529        @Nonnull
530        protected Optional<RequestHandler> getServerListener() {
531                return Optional.ofNullable(this.requestHandler);
532        }
533
534        @Nonnull
535        protected Optional<EventLoop> getEventLoop() {
536                return Optional.ofNullable(this.eventLoop);
537        }
538
539        @Nonnull
540        protected Supplier<ExecutorService> getRequestHandlerExecutorServiceSupplier() {
541                return this.requestHandlerExecutorServiceSupplier;
542        }
543
544        @Nonnull
545        protected Optional<LifecycleInterceptor> getLifecycleInterceptor() {
546                return Optional.ofNullable(this.lifecycleInterceptor);
547        }
548
549        @Nonnull
550        protected Optional<RequestHandler> getRequestHandler() {
551                return Optional.ofNullable(this.requestHandler);
552        }
553
554        @ThreadSafe
555        protected static class NonvirtualThreadFactory implements ThreadFactory {
556                @Nonnull
557                private final String namePrefix;
558                @Nonnull
559                private final AtomicInteger idGenerator;
560
561                public NonvirtualThreadFactory(@Nonnull String namePrefix) {
562                        requireNonNull(namePrefix);
563
564                        this.namePrefix = namePrefix;
565                        this.idGenerator = new AtomicInteger(0);
566                }
567
568                @Override
569                @Nonnull
570                public Thread newThread(@Nonnull Runnable runnable) {
571                        String name = format("%s-%s", getNamePrefix(), getIdGenerator().incrementAndGet());
572                        return new Thread(runnable, name);
573                }
574
575                @Nonnull
576                protected String getNamePrefix() {
577                        return this.namePrefix;
578                }
579
580                @Nonnull
581                protected AtomicInteger getIdGenerator() {
582                        return this.idGenerator;
583                }
584        }
585
586        /**
587         * Builder used to construct instances of {@link DefaultServer}.
588         * <p>
589         * This class is intended for use by a single thread.
590         *
591         * @author <a href="https://www.revetkn.com">Mark Allen</a>
592         */
593        @NotThreadSafe
594        public static class Builder {
595                @Nonnull
596                private Integer port;
597                @Nullable
598                private String host;
599                @Nullable
600                private Integer concurrency;
601                @Nullable
602                private Duration requestTimeout;
603                @Nullable
604                private Duration socketSelectTimeout;
605                @Nullable
606                private Duration shutdownTimeout;
607                @Nullable
608                private Integer maximumRequestSizeInBytes;
609                @Nullable
610                private Integer requestReadBufferSizeInBytes;
611                @Nullable
612                private Integer socketPendingConnectionLimit;
613                @Nullable
614                private MultipartParser multipartParser;
615                @Nullable
616                private Supplier<ExecutorService> requestHandlerExecutorServiceSupplier;
617
618                @Nonnull
619                protected Builder(@Nonnull Integer port) {
620                        requireNonNull(port);
621                        this.port = port;
622                }
623
624                @Nonnull
625                public Builder port(@Nonnull Integer port) {
626                        requireNonNull(port);
627                        this.port = port;
628                        return this;
629                }
630
631                @Nonnull
632                public Builder host(@Nullable String host) {
633                        this.host = host;
634                        return this;
635                }
636
637                @Nonnull
638                public Builder concurrency(@Nullable Integer concurrency) {
639                        this.concurrency = concurrency;
640                        return this;
641                }
642
643                @Nonnull
644                public Builder requestTimeout(@Nullable Duration requestTimeout) {
645                        this.requestTimeout = requestTimeout;
646                        return this;
647                }
648
649                @Nonnull
650                public Builder socketSelectTimeout(@Nullable Duration socketSelectTimeout) {
651                        this.socketSelectTimeout = socketSelectTimeout;
652                        return this;
653                }
654
655                @Nonnull
656                public Builder socketPendingConnectionLimit(@Nullable Integer socketPendingConnectionLimit) {
657                        this.socketPendingConnectionLimit = socketPendingConnectionLimit;
658                        return this;
659                }
660
661                @Nonnull
662                public Builder shutdownTimeout(@Nullable Duration shutdownTimeout) {
663                        this.shutdownTimeout = shutdownTimeout;
664                        return this;
665                }
666
667                @Nonnull
668                public Builder maximumRequestSizeInBytes(@Nullable Integer maximumRequestSizeInBytes) {
669                        this.maximumRequestSizeInBytes = maximumRequestSizeInBytes;
670                        return this;
671                }
672
673                @Nonnull
674                public Builder requestReadBufferSizeInBytes(@Nullable Integer requestReadBufferSizeInBytes) {
675                        this.requestReadBufferSizeInBytes = requestReadBufferSizeInBytes;
676                        return this;
677                }
678
679                @Nonnull
680                public Builder multipartParser(@Nullable MultipartParser multipartParser) {
681                        this.multipartParser = multipartParser;
682                        return this;
683                }
684
685                @Nonnull
686                public Builder requestHandlerExecutorServiceSupplier(@Nullable Supplier<ExecutorService> requestHandlerExecutorServiceSupplier) {
687                        this.requestHandlerExecutorServiceSupplier = requestHandlerExecutorServiceSupplier;
688                        return this;
689                }
690
691                @Nonnull
692                public DefaultServer build() {
693                        return new DefaultServer(this);
694                }
695        }
696}