forked from rohitdash08/FinMind
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompression.py
More file actions
99 lines (75 loc) · 3.19 KB
/
compression.py
File metadata and controls
99 lines (75 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"""
API response compression & payload optimization (Issue #129).
Provides gzip compression for JSON/text responses via Flask after_request hook.
Uses Python's built-in gzip — no extra dependencies required.
Features:
- Gzip when client sends Accept-Encoding: gzip
- Skips compression for small responses (< MIN_SIZE bytes)
- Skips already-encoded responses
- Skips non-compressible content types (images, binary)
- Tracks compression ratio in X-Compression-Ratio header (dev/debug)
"""
from __future__ import annotations
import gzip
import logging
from flask import Flask, Request, Response, current_app
logger = logging.getLogger("finmind.compression")
# Minimum payload size (bytes) before we bother compressing
MIN_COMPRESS_SIZE = 512
# Content types that benefit from compression
_COMPRESSIBLE = {
"application/json",
"text/html",
"text/plain",
"text/csv",
"application/javascript",
"text/javascript",
"application/xml",
"text/xml",
}
def init_compression(app: Flask) -> None:
"""Register the compression after_request hook with the Flask app."""
@app.after_request
def compress_response(response: Response) -> Response:
return _maybe_compress(response)
logger.info("Response compression enabled (min_size=%d bytes)", MIN_COMPRESS_SIZE)
def _maybe_compress(response: Response) -> Response:
# Already compressed or explicitly opted out
if response.headers.get("Content-Encoding"):
return response
# Skip streaming responses: calling get_data() on a direct_passthrough /
# stream_with_context response would buffer the entire stream in memory,
# defeating the purpose of streaming and potentially exhausting RAM on large
# payloads. Streaming responses must be compressed at the WSGI/proxy layer
# (e.g. nginx gzip_proxied) instead.
if response.direct_passthrough:
return response
# Client must accept gzip
from flask import request as current_request
accept_encoding = current_request.headers.get("Accept-Encoding", "")
if "gzip" not in accept_encoding.lower():
return response
# Only compress compressible content types
content_type = response.content_type.split(";")[0].strip()
if content_type not in _COMPRESSIBLE:
return response
# Get response data (force evaluation of lazy responses)
data = response.get_data()
# Skip tiny payloads — compression overhead isn't worth it
if len(data) < MIN_COMPRESS_SIZE:
return response
compressed = gzip.compress(data, compresslevel=6)
# Only use compressed version if it's actually smaller
if len(compressed) >= len(data):
return response
ratio = round(1 - len(compressed) / len(data), 3)
response.set_data(compressed)
response.headers["Content-Encoding"] = "gzip"
response.headers["Content-Length"] = len(compressed)
response.headers["Vary"] = "Accept-Encoding"
# X-Compression-Ratio is a debug-only header: it reveals payload size
# information that could aid an attacker (e.g. BREACH-style attacks).
# Only emit it when the application is running in debug mode.
if current_app.debug:
response.headers["X-Compression-Ratio"] = str(ratio)
return response