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 org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import java.time.Duration; 024import java.util.concurrent.ExecutorService; 025import java.util.function.Consumer; 026import java.util.function.Supplier; 027 028import static java.util.Objects.requireNonNull; 029 030/** 031 * Contract for HTTP server implementations that are designed to be managed by a {@link com.soklet.Soklet} instance. 032 * <p> 033 * <strong>Most Soklet applications will use the default {@link HttpServer} (constructed via the {@link #withPort(Integer)} builder factory method) and therefore do not need to implement this interface directly.</strong> 034 * <p> 035 * For example: 036 * <pre>{@code SokletConfig config = SokletConfig.withHttpServer( 037 * HttpServer.fromPort(8080) 038 * ).build(); 039 * 040 * try (Soklet soklet = Soklet.fromConfig(config)) { 041 * soklet.start(); 042 * System.out.println("Soklet started, press [enter] to exit"); 043 * soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY); 044 * }}</pre> 045 * 046 * @author <a href="https://www.revetkn.com">Mark Allen</a> 047 */ 048public interface HttpServer extends AutoCloseable { 049 /** 050 * Starts the server, which makes it able to accept requests from clients. 051 * <p> 052 * If the server is already started, no action is taken. 053 * <p> 054 * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong> 055 */ 056 void start(); 057 058 /** 059 * Stops the server, which makes it unable to accept requests from clients. 060 * <p> 061 * If the server is already stopped, no action is taken. 062 * <p> 063 * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong> 064 */ 065 void stop(); 066 067 /** 068 * Is this server started (that is, able to handle requests from clients)? 069 * 070 * @return {@code true} if the server is started, {@code false} otherwise 071 */ 072 @NonNull 073 Boolean isStarted(); 074 075 /** 076 * The {@link com.soklet.Soklet} instance which manages this {@link HttpServer} will invoke this method exactly once at initialization time - this allows {@link com.soklet.Soklet} to "talk" to your {@link HttpServer}. 077 * <p> 078 * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong> 079 * 080 * @param sokletConfig configuration for the Soklet instance that controls this server 081 * @param requestHandler a {@link com.soklet.Soklet}-internal request handler which takes a {@link HttpServer}-provided request as input and supplies a {@link MarshaledResponse} as output for the {@link HttpServer} to write back to the client 082 */ 083 void initialize(@NonNull SokletConfig sokletConfig, 084 @NonNull RequestHandler requestHandler); 085 086 /** 087 * {@link AutoCloseable}-enabled synonym for {@link #stop()}. 088 * <p> 089 * <strong>This method is designed for internal use by {@link com.soklet.Soklet} only and should not be invoked elsewhere.</strong> 090 * 091 * @throws Exception if an exception occurs while stopping the server 092 */ 093 @Override 094 default void close() throws Exception { 095 stop(); 096 } 097 098 /** 099 * Request/response processing contract for {@link HttpServer} implementations. 100 * <p> 101 * This is used internally by {@link com.soklet.Soklet} instances to "talk" to a {@link HttpServer} via {@link HttpServer#initialize(SokletConfig, RequestHandler)}. It's the responsibility of the {@link HttpServer} to implement HTTP mechanics: read bytes from the request, write bytes to the response, and so forth. 102 * <p> 103 * <strong>Most Soklet applications will use Soklet's default {@link HttpServer} implementation and therefore do not need to implement this interface directly.</strong> 104 * 105 * @author <a href="https://www.revetkn.com">Mark Allen</a> 106 */ 107 @FunctionalInterface 108 interface RequestHandler { 109 /** 110 * Callback to be invoked by a {@link HttpServer} implementation after it has received an HTTP request but prior to writing an HTTP response. 111 * <p> 112 * The {@link HttpServer} is responsible for converting its internal request representation into a {@link Request}, which a {@link com.soklet.Soklet} instance consumes and performs Soklet application request processing logic. 113 * <p> 114 * The {@link com.soklet.Soklet} instance will generate a {@link MarshaledResponse} for the request, which it "hands back" to the {@link HttpServer} to be sent over the wire to the client. 115 * 116 * @param request a Soklet {@link Request} representation of the {@link HttpServer}'s internal HTTP request data 117 * @param requestResultConsumer invoked by {@link com.soklet.Soklet} when it's time for the {@link HttpServer} to write HTTP response data to the client 118 */ 119 void handleRequest(@NonNull Request request, 120 @NonNull Consumer<HttpRequestResult> requestResultConsumer); 121 } 122 123 /** 124 * Acquires a builder for {@link HttpServer} instances. 125 * 126 * @param port the port number on which the server should listen 127 * @return the builder 128 */ 129 @NonNull 130 static Builder withPort(@NonNull Integer port) { 131 requireNonNull(port); 132 return new Builder(port); 133 } 134 135 /** 136 * Creates a {@link HttpServer} configured with the given port and default settings. 137 * 138 * @param port the port number on which the server should listen 139 * @return a {@link HttpServer} instance 140 */ 141 @NonNull 142 static HttpServer fromPort(@NonNull Integer port) { 143 return withPort(port).build(); 144 } 145 146 /** 147 * Builder used to construct a standard implementation of {@link HttpServer}. 148 * <p> 149 * This class is intended for use by a single thread. 150 * 151 * @author <a href="https://www.revetkn.com">Mark Allen</a> 152 */ 153 @NotThreadSafe 154 final class Builder { 155 @NonNull 156 Integer port; 157 @Nullable 158 String host; 159 @Nullable 160 Integer concurrency; 161 @Nullable 162 Duration requestHeaderTimeout; 163 @Nullable 164 Duration requestBodyTimeout; 165 @Nullable 166 Duration responseWriteIdleTimeout; 167 @Nullable 168 ResponseGzipPolicy responseGzipPolicy; 169 @Nullable 170 Duration requestHandlerTimeout; 171 @Nullable 172 Integer requestHandlerConcurrency; 173 @Nullable 174 Integer requestHandlerQueueCapacity; 175 @Nullable 176 Duration socketSelectTimeout; 177 @Nullable 178 Duration shutdownTimeout; 179 @Nullable 180 Integer maximumRequestSizeInBytes; 181 @Nullable 182 Integer maximumHeaderCount; 183 @Nullable 184 Integer maximumHeadersSizeInBytes; 185 @Nullable 186 Integer maximumRequestTargetLengthInBytes; 187 @Nullable 188 Integer requestReadBufferSizeInBytes; 189 @Nullable 190 Integer socketPendingConnectionLimit; 191 @Nullable 192 Integer concurrentConnectionLimit; 193 @Nullable 194 MultipartParser multipartParser; 195 @Nullable 196 Supplier<ExecutorService> requestHandlerExecutorServiceSupplier; 197 @Nullable 198 Supplier<ExecutorService> streamingExecutorServiceSupplier; 199 @Nullable 200 Integer streamingQueueCapacityInBytes; 201 @Nullable 202 Integer streamingChunkSizeInBytes; 203 @Nullable 204 Duration streamingResponseTimeout; 205 @Nullable 206 Duration streamingResponseIdleTimeout; 207 @Nullable 208 IdGenerator<?> idGenerator; 209 210 private Builder(@NonNull Integer port) { 211 requireNonNull(port); 212 this.port = port; 213 } 214 215 @NonNull 216 public Builder port(@NonNull Integer port) { 217 requireNonNull(port); 218 this.port = port; 219 return this; 220 } 221 222 @NonNull 223 public Builder host(@Nullable String host) { 224 this.host = host; 225 return this; 226 } 227 228 @NonNull 229 public Builder concurrency(@Nullable Integer concurrency) { 230 this.concurrency = concurrency; 231 return this; 232 } 233 234 /** 235 * Sets the maximum duration for reading the HTTP request line and headers. 236 * <p> 237 * If this value is not specified, Soklet uses the server default. 238 * 239 * @param requestHeaderTimeout the request header timeout, or {@code null} for the default 240 * @return this builder 241 */ 242 @NonNull 243 public Builder requestHeaderTimeout(@Nullable Duration requestHeaderTimeout) { 244 this.requestHeaderTimeout = requestHeaderTimeout; 245 return this; 246 } 247 248 /** 249 * Sets the maximum duration for reading the HTTP request body after the request 250 * line and headers have been received. 251 * <p> 252 * If this value is not specified, Soklet uses the server default. 253 * 254 * @param requestBodyTimeout the request body timeout, or {@code null} for the default 255 * @return this builder 256 */ 257 @NonNull 258 public Builder requestBodyTimeout(@Nullable Duration requestBodyTimeout) { 259 this.requestBodyTimeout = requestBodyTimeout; 260 return this; 261 } 262 263 /** 264 * Sets the maximum idle duration while writing a non-streaming HTTP response. 265 * <p> 266 * The timeout is reset each time response bytes are written to the socket. 267 * Use {@link Duration#ZERO} to disable this timeout. 268 * <p> 269 * If this value is not specified, Soklet uses the server default. 270 * 271 * @param responseWriteIdleTimeout the response write idle timeout, or {@code null} for the default 272 * @return this builder 273 */ 274 @NonNull 275 public Builder responseWriteIdleTimeout(@Nullable Duration responseWriteIdleTimeout) { 276 this.responseWriteIdleTimeout = responseWriteIdleTimeout; 277 return this; 278 } 279 280 /** 281 * Sets the policy used by the standard HTTP server to decide whether eligible finalized 282 * in-memory responses should be gzipped. 283 * <p> 284 * Soklet invokes this policy only after its own HTTP protocol checks pass. For example, 285 * {@code Accept-Encoding} must permit {@code gzip}, and Soklet will skip streaming, file, 286 * range, already-encoded, transfer-encoded, bodyless, and otherwise ineligible responses. 287 * If this value is not specified, response gzip is disabled. 288 * 289 * @param responseGzipPolicy the response gzip policy to use, or {@code null} for the default 290 * @return this builder 291 */ 292 @NonNull 293 public Builder responseGzipPolicy(@Nullable ResponseGzipPolicy responseGzipPolicy) { 294 this.responseGzipPolicy = responseGzipPolicy; 295 return this; 296 } 297 298 @NonNull 299 public Builder requestHandlerTimeout(@Nullable Duration requestHandlerTimeout) { 300 this.requestHandlerTimeout = requestHandlerTimeout; 301 return this; 302 } 303 304 @NonNull 305 public Builder requestHandlerConcurrency(@Nullable Integer requestHandlerConcurrency) { 306 this.requestHandlerConcurrency = requestHandlerConcurrency; 307 return this; 308 } 309 310 @NonNull 311 public Builder requestHandlerQueueCapacity(@Nullable Integer requestHandlerQueueCapacity) { 312 this.requestHandlerQueueCapacity = requestHandlerQueueCapacity; 313 return this; 314 } 315 316 @NonNull 317 public Builder socketSelectTimeout(@Nullable Duration socketSelectTimeout) { 318 this.socketSelectTimeout = socketSelectTimeout; 319 return this; 320 } 321 322 @NonNull 323 public Builder socketPendingConnectionLimit(@Nullable Integer socketPendingConnectionLimit) { 324 this.socketPendingConnectionLimit = socketPendingConnectionLimit; 325 return this; 326 } 327 328 @NonNull 329 public Builder concurrentConnectionLimit(@Nullable Integer concurrentConnectionLimit) { 330 this.concurrentConnectionLimit = concurrentConnectionLimit; 331 return this; 332 } 333 334 @NonNull 335 public Builder shutdownTimeout(@Nullable Duration shutdownTimeout) { 336 this.shutdownTimeout = shutdownTimeout; 337 return this; 338 } 339 340 /** 341 * Sets the maximum accepted HTTP request size in bytes. 342 * <p> 343 * This limit applies to the whole received HTTP request, including request line, 344 * headers, transfer framing, and body bytes. Applications that think in terms of 345 * payload size should leave room for request metadata and protocol framing. 346 * 347 * @param maximumRequestSizeInBytes the maximum request size, or {@code null} for the default 348 * @return this builder 349 */ 350 @NonNull 351 public Builder maximumRequestSizeInBytes(@Nullable Integer maximumRequestSizeInBytes) { 352 this.maximumRequestSizeInBytes = maximumRequestSizeInBytes; 353 return this; 354 } 355 356 /** 357 * Sets the maximum number of HTTP header fields accepted in one request. 358 * 359 * @param maximumHeaderCount the maximum header count, or {@code null} for the default 360 * @return this builder 361 */ 362 @NonNull 363 public Builder maximumHeaderCount(@Nullable Integer maximumHeaderCount) { 364 this.maximumHeaderCount = maximumHeaderCount; 365 return this; 366 } 367 368 /** 369 * Sets the maximum accepted HTTP header-section size in bytes. 370 * <p> 371 * This limit applies to the header bytes after the request line, including 372 * header-field line endings and the terminating blank line. 373 * 374 * @param maximumHeadersSizeInBytes the maximum headers size, or {@code null} for the default 375 * @return this builder 376 */ 377 @NonNull 378 public Builder maximumHeadersSizeInBytes(@Nullable Integer maximumHeadersSizeInBytes) { 379 this.maximumHeadersSizeInBytes = maximumHeadersSizeInBytes; 380 return this; 381 } 382 383 /** 384 * Sets the maximum request-target length accepted in bytes. 385 * 386 * @param maximumRequestTargetLengthInBytes the maximum request-target length, or {@code null} for the default 387 * @return this builder 388 */ 389 @NonNull 390 public Builder maximumRequestTargetLengthInBytes(@Nullable Integer maximumRequestTargetLengthInBytes) { 391 this.maximumRequestTargetLengthInBytes = maximumRequestTargetLengthInBytes; 392 return this; 393 } 394 395 @NonNull 396 public Builder requestReadBufferSizeInBytes(@Nullable Integer requestReadBufferSizeInBytes) { 397 this.requestReadBufferSizeInBytes = requestReadBufferSizeInBytes; 398 return this; 399 } 400 401 @NonNull 402 public Builder multipartParser(@Nullable MultipartParser multipartParser) { 403 this.multipartParser = multipartParser; 404 return this; 405 } 406 407 @NonNull 408 public Builder requestHandlerExecutorServiceSupplier(@Nullable Supplier<ExecutorService> requestHandlerExecutorServiceSupplier) { 409 this.requestHandlerExecutorServiceSupplier = requestHandlerExecutorServiceSupplier; 410 return this; 411 } 412 413 /** 414 * Sets the executor service supplier used to run streaming response producers. 415 * 416 * @param streamingExecutorServiceSupplier the executor service supplier, or {@code null} for the default 417 * @return this builder 418 */ 419 @NonNull 420 public Builder streamingExecutorServiceSupplier(@Nullable Supplier<ExecutorService> streamingExecutorServiceSupplier) { 421 this.streamingExecutorServiceSupplier = streamingExecutorServiceSupplier; 422 return this; 423 } 424 425 /** 426 * Sets the per-stream producer queue capacity in bytes. 427 * 428 * @param streamingQueueCapacityInBytes the queue capacity, or {@code null} for the default 429 * @return this builder 430 */ 431 @NonNull 432 public Builder streamingQueueCapacityInBytes(@Nullable Integer streamingQueueCapacityInBytes) { 433 this.streamingQueueCapacityInBytes = streamingQueueCapacityInBytes; 434 return this; 435 } 436 437 /** 438 * Sets the maximum payload chunk size used for HTTP/1.1 chunked streaming. 439 * 440 * @param streamingChunkSizeInBytes the payload chunk size, or {@code null} for the default 441 * @return this builder 442 */ 443 @NonNull 444 public Builder streamingChunkSizeInBytes(@Nullable Integer streamingChunkSizeInBytes) { 445 this.streamingChunkSizeInBytes = streamingChunkSizeInBytes; 446 return this; 447 } 448 449 /** 450 * Sets the maximum total duration for a streaming response. 451 * <p> 452 * Use {@link Duration#ZERO} to disable the timeout. 453 * 454 * @param streamingResponseTimeout the streaming response timeout, or {@code null} for the default 455 * @return this builder 456 */ 457 @NonNull 458 public Builder streamingResponseTimeout(@Nullable Duration streamingResponseTimeout) { 459 this.streamingResponseTimeout = streamingResponseTimeout; 460 return this; 461 } 462 463 /** 464 * Sets the maximum idle duration between bytes produced for a streaming response. 465 * <p> 466 * Use {@link Duration#ZERO} to disable the timeout. 467 * 468 * @param streamingResponseIdleTimeout the streaming response idle timeout, or {@code null} for the default 469 * @return this builder 470 */ 471 @NonNull 472 public Builder streamingResponseIdleTimeout(@Nullable Duration streamingResponseIdleTimeout) { 473 this.streamingResponseIdleTimeout = streamingResponseIdleTimeout; 474 return this; 475 } 476 477 @NonNull 478 public Builder idGenerator(@Nullable IdGenerator<?> idGenerator) { 479 this.idGenerator = idGenerator; 480 return this; 481 } 482 483 @NonNull 484 public HttpServer build() { 485 return new DefaultHttpServer(this); 486 } 487 } 488}