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
This commit is contained in:
Benoit Chesneau 2026-01-22 18:03:14 +01:00
parent ae1eea8108
commit 11c6a97c47
4 changed files with 83 additions and 49 deletions

View File

@ -477,7 +477,7 @@ class AsyncRequest:
self._body_reader = self._chunked_body_reader() self._body_reader = self._chunked_body_reader()
try: try:
return await self._body_reader.__anext__() return await anext(self._body_reader)
except StopAsyncIteration: except StopAsyncIteration:
self._body_remaining = 0 self._body_remaining = 0
return b"" return b""

View File

@ -10,9 +10,6 @@ to ASGI applications.
""" """
import asyncio import asyncio
import base64
import hashlib
import traceback
from datetime import datetime from datetime import datetime
from gunicorn.asgi.unreader import AsyncUnreader from gunicorn.asgi.unreader import AsyncUnreader
@ -20,6 +17,22 @@ from gunicorn.asgi.message import AsyncRequest
from gunicorn.http.errors import NoMoreData 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): class ASGIProtocol(asyncio.Protocol):
"""HTTP/1.1 protocol handler for ASGI applications. """HTTP/1.1 protocol handler for ASGI applications.
@ -97,7 +110,7 @@ class ASGIProtocol(asyncio.Protocol):
if self._is_websocket_upgrade(request): if self._is_websocket_upgrade(request):
await self._handle_websocket(request, sockname, peername) await self._handle_websocket(request, sockname, peername)
break # WebSocket takes over the connection break # WebSocket takes over the connection
else:
# Handle HTTP request # Handle HTTP request
keepalive = await self._handle_http_request( keepalive = await self._handle_http_request(
request, sockname, peername request, sockname, peername
@ -155,9 +168,13 @@ class ASGIProtocol(asyncio.Protocol):
scope = self._build_http_scope(request, sockname, peername) scope = self._build_http_scope(request, sockname, peername)
response_started = False response_started = False
response_complete = False response_complete = False
body_parts = []
exc_to_raise = None exc_to_raise = None
# Response tracking for access logging
response_status = 500
response_headers = []
response_sent = 0
# Receive queue for body # Receive queue for body
receive_queue = asyncio.Queue() receive_queue = asyncio.Queue()
@ -177,6 +194,7 @@ class ASGIProtocol(asyncio.Protocol):
async def send(message): async def send(message):
nonlocal response_started, response_complete, exc_to_raise nonlocal response_started, response_complete, exc_to_raise
nonlocal response_status, response_headers, response_sent
msg_type = message["type"] msg_type = message["type"]
@ -185,9 +203,9 @@ class ASGIProtocol(asyncio.Protocol):
exc_to_raise = RuntimeError("Response already started") exc_to_raise = RuntimeError("Response already started")
return return
response_started = True response_started = True
status = message["status"] response_status = message["status"]
headers = message.get("headers", []) response_headers = message.get("headers", [])
await self._send_response_start(status, headers, request) await self._send_response_start(response_status, response_headers, request)
elif msg_type == "http.response.body": elif msg_type == "http.response.body":
if not response_started: if not response_started:
@ -202,32 +220,42 @@ class ASGIProtocol(asyncio.Protocol):
if body: if body:
await self._send_body(body) await self._send_body(body)
response_sent += len(body)
if not more_body: if not more_body:
response_complete = True response_complete = True
# Build environ for logging
environ = self._build_environ(request, sockname, peername)
resp = None
try: try:
request_start = datetime.now() request_start = datetime.now()
self.cfg.pre_request(self.worker, request) self.cfg.pre_request(self.worker, request)
await self.app(scope, receive, send) await self.app(scope, receive, send)
if exc_to_raise: if exc_to_raise is not None:
raise exc_to_raise raise exc_to_raise
# Ensure response was sent # Ensure response was sent
if not response_started: if not response_started:
await self._send_error_response(500, "Internal Server Error") 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") self.log.exception("Error in ASGI application")
if not response_started: if not response_started:
await self._send_error_response(500, "Internal Server Error") await self._send_error_response(500, "Internal Server Error")
response_status = 500
return False return False
finally: finally:
try: try:
request_time = datetime.now() - request_start 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: except Exception:
self.log.exception("Exception in post_request hook") self.log.exception("Exception in post_request hook")
@ -291,6 +319,24 @@ class ASGIProtocol(asyncio.Protocol):
return scope 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): def _build_websocket_scope(self, request, sockname, peername):
"""Build ASGI WebSocket scope from parsed request.""" """Build ASGI WebSocket scope from parsed request."""
# Build headers list as bytes tuples # Build headers list as bytes tuples
@ -334,9 +380,6 @@ class ASGIProtocol(asyncio.Protocol):
# Build headers # Build headers
header_lines = [] header_lines = []
has_content_length = False
has_transfer_encoding = False
has_connection = False
for name, value in headers: for name, value in headers:
if isinstance(name, bytes): if isinstance(name, bytes):
@ -344,13 +387,6 @@ class ASGIProtocol(asyncio.Protocol):
if isinstance(value, bytes): if isinstance(value, bytes):
value = value.decode("latin-1") value = value.decode("latin-1")
header_lines.append(f"{name}: {value}\r\n") 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 # Add server header if not present
header_lines.append("Server: gunicorn/asgi\r\n") header_lines.append("Server: gunicorn/asgi\r\n")

View File

@ -12,7 +12,6 @@ import asyncio
import base64 import base64
import hashlib import hashlib
import struct import struct
import os
# WebSocket frame opcodes # WebSocket frame opcodes
@ -81,7 +80,7 @@ class WebSocketProtocol:
try: try:
await self.app(self.scope, self._receive, self._send) await self.app(self.scope, self._receive, self._send)
except Exception as e: except Exception:
self.log.exception("Error in WebSocket ASGI application") self.log.exception("Error in WebSocket ASGI application")
finally: finally:
read_task.cancel() read_task.cancel()
@ -180,7 +179,8 @@ class WebSocketProtocol:
if opcode == OPCODE_CLOSE: if opcode == OPCODE_CLOSE:
await self._handle_close(payload) await self._handle_close(payload)
break break
elif opcode == OPCODE_PING:
if opcode == OPCODE_PING:
await self._send_frame(OPCODE_PONG, payload) await self._send_frame(OPCODE_PONG, payload)
elif opcode == OPCODE_PONG: elif opcode == OPCODE_PONG:
# Ignore pongs # Ignore pongs
@ -212,7 +212,7 @@ class WebSocketProtocol:
"code": self.close_code or CLOSE_ABNORMAL, "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. """Read a single WebSocket frame.
Returns: Returns:
@ -326,10 +326,9 @@ class WebSocketProtocol:
self.closed = True 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).""" """Handle continuation frame (already processed in _read_frame)."""
# This is called for partial fragments, nothing to do # This is called for partial fragments, nothing to do here
pass
async def _send_frame(self, opcode, payload): async def _send_frame(self, opcode, payload):
"""Send a WebSocket frame. """Send a WebSocket frame.

View File

@ -12,7 +12,6 @@ HTTP parsing infrastructure.
import asyncio import asyncio
import os import os
import signal import signal
import ssl
import sys import sys
from gunicorn.workers import base from gunicorn.workers import base