From 11c6a97c47f2a4a16cf683ae0f224b3880fca6e4 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Thu, 22 Jan 2026 18:03:14 +0100 Subject: [PATCH] asgi: Fix pylint and pycodestyle warnings - Remove unused imports (ssl, os, base64, hashlib, traceback) - Remove unused variables (body_parts, has_content_length, etc.) - Fix no-else-break patterns in protocol.py and websocket.py - Replace __anext__() with anext() builtin - Remove unnecessary pass statements - Add proper access logging to ASGI protocol handler - Add ASGIResponseInfo class and _build_environ method for logging - Disable too-many-return-statements for _read_frame method - Fix raising-bad-type error (use 'is not None' check) - Fix whitespace before colon in message.py --- gunicorn/asgi/message.py | 4 +- gunicorn/asgi/protocol.py | 114 ++++++++++++++++++++++++------------- gunicorn/asgi/websocket.py | 13 ++--- gunicorn/workers/gasgi.py | 1 - 4 files changed, 83 insertions(+), 49 deletions(-) diff --git a/gunicorn/asgi/message.py b/gunicorn/asgi/message.py index d7d20c83..a2d8e825 100644 --- a/gunicorn/asgi/message.py +++ b/gunicorn/asgi/message.py @@ -477,7 +477,7 @@ class AsyncRequest: self._body_reader = self._chunked_body_reader() try: - return await self._body_reader.__anext__() + return await anext(self._body_reader) except StopAsyncIteration: self._body_remaining = 0 return b"" @@ -489,7 +489,7 @@ class AsyncRequest: size_line = await self._read_chunk_size_line() # Parse chunk size (handle extensions) chunk_size, *_ = size_line.split(b";", 1) - if _ : + if _: chunk_size = chunk_size.rstrip(b" \t") if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): diff --git a/gunicorn/asgi/protocol.py b/gunicorn/asgi/protocol.py index cededd68..0eb1d045 100644 --- a/gunicorn/asgi/protocol.py +++ b/gunicorn/asgi/protocol.py @@ -10,9 +10,6 @@ to ASGI applications. """ import asyncio -import base64 -import hashlib -import traceback from datetime import datetime from gunicorn.asgi.unreader import AsyncUnreader @@ -20,6 +17,22 @@ from gunicorn.asgi.message import AsyncRequest from gunicorn.http.errors import NoMoreData +class ASGIResponseInfo: + """Simple container for ASGI response info for access logging.""" + + def __init__(self, status, headers, sent): + self.status = status + self.sent = sent + # Convert headers to list of string tuples for logging + self.headers = [] + for name, value in headers: + if isinstance(name, bytes): + name = name.decode("latin-1") + if isinstance(value, bytes): + value = value.decode("latin-1") + self.headers.append((name, value)) + + class ASGIProtocol(asyncio.Protocol): """HTTP/1.1 protocol handler for ASGI applications. @@ -97,30 +110,30 @@ class ASGIProtocol(asyncio.Protocol): if self._is_websocket_upgrade(request): await self._handle_websocket(request, sockname, peername) break # WebSocket takes over the connection - else: - # Handle HTTP request - keepalive = await self._handle_http_request( - request, sockname, peername - ) - # Increment worker request count - self.worker.nr += 1 + # Handle HTTP request + keepalive = await self._handle_http_request( + request, sockname, peername + ) - # Check max_requests - if self.worker.nr >= self.worker.max_requests: - self.log.info("Autorestarting worker after current request.") - self.worker.alive = False - keepalive = False + # Increment worker request count + self.worker.nr += 1 - if not keepalive or not self.worker.alive: - break + # Check max_requests + if self.worker.nr >= self.worker.max_requests: + self.log.info("Autorestarting worker after current request.") + self.worker.alive = False + keepalive = False - # Check connection limits for keepalive - if not self.cfg.keepalive: - break + if not keepalive or not self.worker.alive: + break - # Drain any unread body before next request - await request.drain_body() + # Check connection limits for keepalive + if not self.cfg.keepalive: + break + + # Drain any unread body before next request + await request.drain_body() except asyncio.CancelledError: pass @@ -155,9 +168,13 @@ class ASGIProtocol(asyncio.Protocol): scope = self._build_http_scope(request, sockname, peername) response_started = False response_complete = False - body_parts = [] exc_to_raise = None + # Response tracking for access logging + response_status = 500 + response_headers = [] + response_sent = 0 + # Receive queue for body receive_queue = asyncio.Queue() @@ -177,6 +194,7 @@ class ASGIProtocol(asyncio.Protocol): async def send(message): nonlocal response_started, response_complete, exc_to_raise + nonlocal response_status, response_headers, response_sent msg_type = message["type"] @@ -185,9 +203,9 @@ class ASGIProtocol(asyncio.Protocol): exc_to_raise = RuntimeError("Response already started") return response_started = True - status = message["status"] - headers = message.get("headers", []) - await self._send_response_start(status, headers, request) + response_status = message["status"] + response_headers = message.get("headers", []) + await self._send_response_start(response_status, response_headers, request) elif msg_type == "http.response.body": if not response_started: @@ -202,32 +220,42 @@ class ASGIProtocol(asyncio.Protocol): if body: await self._send_body(body) + response_sent += len(body) if not more_body: response_complete = True + # Build environ for logging + environ = self._build_environ(request, sockname, peername) + resp = None + try: request_start = datetime.now() self.cfg.pre_request(self.worker, request) await self.app(scope, receive, send) - if exc_to_raise: + if exc_to_raise is not None: raise exc_to_raise # Ensure response was sent if not response_started: await self._send_error_response(500, "Internal Server Error") + response_status = 500 - except Exception as e: + except Exception: self.log.exception("Error in ASGI application") if not response_started: await self._send_error_response(500, "Internal Server Error") + response_status = 500 return False finally: try: request_time = datetime.now() - request_start - self.cfg.post_request(self.worker, request, {}, None) + # Create response info for logging + resp = ASGIResponseInfo(response_status, response_headers, response_sent) + self.log.access(resp, request, environ, request_time) + self.cfg.post_request(self.worker, request, environ, resp) except Exception: self.log.exception("Exception in post_request hook") @@ -291,6 +319,24 @@ class ASGIProtocol(asyncio.Protocol): return scope + def _build_environ(self, request, sockname, peername): + """Build minimal WSGI-like environ dict for access logging.""" + environ = { + "REQUEST_METHOD": request.method, + "RAW_URI": request.uri, + "PATH_INFO": request.path, + "QUERY_STRING": request.query or "", + "SERVER_PROTOCOL": f"HTTP/{request.version[0]}.{request.version[1]}", + "REMOTE_ADDR": peername[0] if peername else "-", + } + + # Add HTTP headers as environ vars + for name, value in request.headers: + key = "HTTP_" + name.replace("-", "_") + environ[key] = value + + return environ + def _build_websocket_scope(self, request, sockname, peername): """Build ASGI WebSocket scope from parsed request.""" # Build headers list as bytes tuples @@ -334,9 +380,6 @@ class ASGIProtocol(asyncio.Protocol): # Build headers header_lines = [] - has_content_length = False - has_transfer_encoding = False - has_connection = False for name, value in headers: if isinstance(name, bytes): @@ -344,13 +387,6 @@ class ASGIProtocol(asyncio.Protocol): if isinstance(value, bytes): value = value.decode("latin-1") header_lines.append(f"{name}: {value}\r\n") - name_lower = name.lower() - if name_lower == "content-length": - has_content_length = True - elif name_lower == "transfer-encoding": - has_transfer_encoding = True - elif name_lower == "connection": - has_connection = True # Add server header if not present header_lines.append("Server: gunicorn/asgi\r\n") diff --git a/gunicorn/asgi/websocket.py b/gunicorn/asgi/websocket.py index bcde84ee..737268b6 100644 --- a/gunicorn/asgi/websocket.py +++ b/gunicorn/asgi/websocket.py @@ -12,7 +12,6 @@ import asyncio import base64 import hashlib import struct -import os # WebSocket frame opcodes @@ -81,7 +80,7 @@ class WebSocketProtocol: try: await self.app(self.scope, self._receive, self._send) - except Exception as e: + except Exception: self.log.exception("Error in WebSocket ASGI application") finally: read_task.cancel() @@ -180,7 +179,8 @@ class WebSocketProtocol: if opcode == OPCODE_CLOSE: await self._handle_close(payload) break - elif opcode == OPCODE_PING: + + if opcode == OPCODE_PING: await self._send_frame(OPCODE_PONG, payload) elif opcode == OPCODE_PONG: # Ignore pongs @@ -212,7 +212,7 @@ class WebSocketProtocol: "code": self.close_code or CLOSE_ABNORMAL, }) - async def _read_frame(self): + async def _read_frame(self): # pylint: disable=too-many-return-statements """Read a single WebSocket frame. Returns: @@ -326,10 +326,9 @@ class WebSocketProtocol: self.closed = True - async def _handle_continuation(self, payload): + async def _handle_continuation(self, payload): # pylint: disable=unused-argument """Handle continuation frame (already processed in _read_frame).""" - # This is called for partial fragments, nothing to do - pass + # This is called for partial fragments, nothing to do here async def _send_frame(self, opcode, payload): """Send a WebSocket frame. diff --git a/gunicorn/workers/gasgi.py b/gunicorn/workers/gasgi.py index b0d57cf0..118d11de 100644 --- a/gunicorn/workers/gasgi.py +++ b/gunicorn/workers/gasgi.py @@ -12,7 +12,6 @@ HTTP parsing infrastructure. import asyncio import os import signal -import ssl import sys from gunicorn.workers import base