create-caspian-app 0.3.0-rc.2 → 0.3.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.py CHANGED
@@ -87,6 +87,11 @@ REQUEST_TIMEOUT_SECONDS = max(
87
87
  1.0,
88
88
  float(os.getenv('CASPIAN_REQUEST_TIMEOUT_SECONDS', 20)),
89
89
  )
90
+ MAX_CONTENT_LENGTH_BYTES = max(1, MAX_CONTENT_LENGTH_MB) * 1024 * 1024
91
+
92
+
93
+ class RequestBodyTooLarge(Exception):
94
+ pass
90
95
 
91
96
 
92
97
  def _client_error_message(exc: Exception) -> str:
@@ -228,6 +233,67 @@ class SecurityHeadersMiddleware:
228
233
  await self.app(scope, receive, send_wrapper)
229
234
 
230
235
 
236
+ class BodySizeLimitMiddleware:
237
+ """Reject oversized HTTP request bodies before route or RPC parsing."""
238
+
239
+ def __init__(self, app: ASGIApp): self.app = app
240
+
241
+ async def __call__(self, scope: Scope, receive: Receive, send: Send):
242
+ if scope["type"] != "http":
243
+ await self.app(scope, receive, send)
244
+ return
245
+
246
+ headers = MutableHeaders(scope=scope)
247
+ content_length = headers.get("content-length")
248
+ if content_length:
249
+ try:
250
+ if int(content_length) > MAX_CONTENT_LENGTH_BYTES:
251
+ await self._send_too_large(send)
252
+ return
253
+ except ValueError:
254
+ pass
255
+
256
+ received = 0
257
+ response_started = False
258
+
259
+ async def limited_receive():
260
+ nonlocal received
261
+ message = await receive()
262
+ if message["type"] == "http.request":
263
+ received += len(message.get("body", b""))
264
+ if received > MAX_CONTENT_LENGTH_BYTES:
265
+ raise RequestBodyTooLarge()
266
+ return message
267
+
268
+ async def send_wrapper(message):
269
+ nonlocal response_started
270
+ if message["type"] == "http.response.start":
271
+ response_started = True
272
+ await send(message)
273
+
274
+ try:
275
+ await self.app(scope, limited_receive, send_wrapper)
276
+ except RequestBodyTooLarge:
277
+ if not response_started:
278
+ await self._send_too_large(send)
279
+
280
+ async def _send_too_large(self, send: Send):
281
+ response = Response(
282
+ content="Request body too large.",
283
+ status_code=413,
284
+ media_type="text/plain",
285
+ )
286
+
287
+ async def receive_empty_body():
288
+ return {"type": "http.request", "body": b"", "more_body": False}
289
+
290
+ await response(
291
+ {"type": "http", "method": "POST", "path": "/", "headers": []},
292
+ receive=receive_empty_body,
293
+ send=send,
294
+ )
295
+
296
+
231
297
  class AuthMiddleware:
232
298
  """Auth middleware using pure ASGI pattern for proper session handling."""
233
299
 
@@ -318,7 +384,8 @@ class RequestDiagnosticsMiddleware:
318
384
 
319
385
  method = scope.get("method", "GET")
320
386
  path = scope.get("path", "")
321
- should_log = not path.startswith(('/css/', '/js/', '/assets/', '/favicon.ico'))
387
+ should_log = not path.startswith(
388
+ ('/css/', '/js/', '/assets/', '/favicon.ico'))
322
389
  started = time.perf_counter()
323
390
 
324
391
  if should_log and not IS_PRODUCTION:
@@ -349,12 +416,14 @@ class RequestDiagnosticsMiddleware:
349
416
  except Exception:
350
417
  if should_log and not IS_PRODUCTION:
351
418
  elapsed_ms = int((time.perf_counter() - started) * 1000)
352
- print(f"[request:error] {method} {path} after {elapsed_ms}ms", flush=True)
419
+ print(
420
+ f"[request:error] {method} {path} after {elapsed_ms}ms", flush=True)
353
421
  raise
354
422
  finally:
355
423
  if should_log and not IS_PRODUCTION:
356
424
  elapsed_ms = int((time.perf_counter() - started) * 1000)
357
- print(f"[request:end] {method} {path} {elapsed_ms}ms", flush=True)
425
+ print(
426
+ f"[request:end] {method} {path} {elapsed_ms}ms", flush=True)
358
427
 
359
428
  # ====
360
429
  # Route Registration
@@ -674,7 +743,8 @@ async def custom_general_exception_handler(request: Request, exc: Exception):
674
743
  context_data = {'request': request,
675
744
  'error_message': error_message, 'error_trace': error_trace}
676
745
  try:
677
- rendered_content = compile_template(raw_content).render(**context_data)
746
+ rendered_content = compile_template(
747
+ raw_content).render(**context_data)
678
748
  html_output, root_layout_id = await render_with_nested_layouts(
679
749
  children=rendered_content,
680
750
  route_dir='src/app',
@@ -714,9 +784,19 @@ app.add_middleware(
714
784
  https_only=IS_PRODUCTION,
715
785
  path='/',
716
786
  )
787
+ app.add_middleware(BodySizeLimitMiddleware)
717
788
  app.add_middleware(SecurityHeadersMiddleware)
718
- app.add_middleware(RequestDiagnosticsMiddleware)
789
+
790
+ if not IS_PRODUCTION:
791
+ app.add_middleware(RequestDiagnosticsMiddleware)
719
792
 
720
793
  if __name__ == '__main__':
721
794
  port = int(os.getenv('PORT', 5091))
722
- uvicorn.run("main:app", host="0.0.0.0", port=port, reload=False)
795
+ workers = max(1, int(os.getenv('UVICORN_WORKERS', '1')))
796
+ uvicorn.run(
797
+ "main:app",
798
+ host="0.0.0.0",
799
+ port=port,
800
+ reload=False,
801
+ workers=workers,
802
+ )