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()
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):

View File

@ -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")

View File

@ -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.

View File

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