generated from goflash/middleware-template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtypes.go
More file actions
468 lines (438 loc) · 14.6 KB
/
types.go
File metadata and controls
468 lines (438 loc) · 14.6 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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
package compression
import (
"io"
"net/http"
"sync"
)
// CompressionType represents the type of compression to use.
//
// The framework supports multiple compression algorithms, each with different
// characteristics in terms of compression ratio, speed, and browser support.
//
// Example:
//
// // Use gzip compression (widely supported)
// app.Use(compression.Compress(compression.UnifiedConfig{
// Types: []compression.CompressionType{compression.GzipCompression},
// }))
//
// // Use brotli compression (better compression, modern browsers)
// app.Use(compression.Compress(compression.UnifiedConfig{
// Types: []compression.CompressionType{compression.BrotliCompression},
// }))
//
// // Prefer brotli, fallback to gzip
// app.Use(compression.Compress(compression.UnifiedConfig{
// Types: []compression.CompressionType{
// compression.BrotliCompression,
// compression.GzipCompression,
// },
// }))
type CompressionType string
const (
// GzipCompression uses gzip compression algorithm.
//
// Gzip is the most widely supported compression format, available in all
// modern browsers and HTTP clients. It provides good compression ratios
// with reasonable CPU usage.
//
// Compression levels: 1-9 (higher = better compression but slower)
// Browser support: Universal (all modern browsers)
// Compression ratio: Good (typically 60-80% for text content)
//
// Example:
//
// app.Use(compression.Gzip(compression.CompressionConfig{
// Level: 6, // balanced performance
// }))
GzipCompression CompressionType = "gzip"
// BrotliCompression uses brotli compression algorithm.
//
// Brotli is a modern compression algorithm that typically provides better
// compression ratios than gzip, especially for text content like HTML,
// CSS, and JavaScript. It's supported by all modern browsers.
//
// Compression levels: 0-11 (higher = better compression but slower)
// Browser support: Modern browsers (Chrome 50+, Firefox 44+, Safari 11+)
// Compression ratio: Excellent (typically 70-90% for text content)
//
// Note: Requires the github.com/andybalholm/brotli package to be available.
// If not available, the middleware will gracefully fall back to no compression.
//
// Example:
//
// app.Use(compression.Brotli(compression.CompressionConfig{
// Level: 11, // maximum compression
// }))
BrotliCompression CompressionType = "br"
)
// CompressionConfig provides common configuration for all compression types.
//
// This configuration is used by individual compression middlewares (Gzip, Brotli)
// to control compression behavior. For unified compression with multiple types,
// use UnifiedConfig instead.
//
// Example:
//
// // Basic gzip compression
// app.Use(compression.Gzip(compression.CompressionConfig{
// Level: 6,
// MinSize: 1024,
// }))
//
// // High compression for static assets
// app.GET("/static/*", staticHandler, compression.Gzip(compression.CompressionConfig{
// Level: 9, // maximum compression
// MinSize: 256, // compress even small files
// }))
type CompressionConfig struct {
// Level sets the compression level.
//
// For gzip: 1-9 (higher = better compression but slower)
// - 1: Fastest compression, lowest ratio
// - 6: Balanced performance (default)
// - 9: Best compression, slowest
//
// For brotli: 0-11 (higher = better compression but slower)
// - 0: Fastest compression, lowest ratio
// - 6: Balanced performance (default)
// - 11: Best compression, slowest
//
// Default: 6 (balanced performance)
Level int
// MinSize sets the minimum response size to compress (in bytes).
//
// Responses smaller than this threshold are not compressed to avoid
// the overhead of compression setup and headers. This is especially
// important for small API responses where compression overhead
// might exceed the benefits.
//
// Default: 0 (compress all responses)
// Recommended: 1024 bytes for general use, 256 bytes for text content
MinSize int
}
// CompressionWriter represents a compression writer with cleanup functionality.
//
// This struct wraps an io.WriteCloser with a cleanup function that should
// be called when the writer is no longer needed. The cleanup function
// typically returns the writer to a pool for reuse.
//
// Example:
//
// writer, err := provider.GetWriter(level, responseWriter)
// if err != nil {
// return err
// }
// defer writer.Put() // Return to pool
//
// // Use writer.Writer for compression
// _, err = writer.Writer.Write(data)
type CompressionWriter struct {
// Writer is the underlying compression writer.
// Use this for writing compressed data to the response.
Writer io.WriteCloser
// Put is a function that should be called to return the writer to the pool.
// This ensures proper cleanup and prevents memory leaks.
Put func()
}
// CompressionProvider defines the interface for compression implementations.
//
// This interface allows the compression package to work with different
// compression algorithms in a unified way. Each compression type (gzip,
// brotli, etc.) implements this interface.
//
// The interface provides methods for:
// - Creating new compression writers
// - Getting pooled compression writers (for performance)
// - Checking if the compression type is supported
// - Getting the compression type identifier
//
// Example:
//
// // Create a gzip provider
// gzipProvider := compression.NewGzipProvider()
//
// // Check if supported (gzip is always supported)
// if gzipProvider.IsSupported() {
// // Use the provider
// writer, err := gzipProvider.GetWriter(6, responseWriter)
// if err != nil {
// return err
// }
// defer writer.Put()
// }
type CompressionProvider interface {
// NewWriter creates a new compression writer for the given output and level.
//
// This method creates a fresh compression writer without pooling.
// For better performance, prefer GetWriter which uses pooled writers.
//
// Args:
// - w: The output writer to compress to
// - level: The compression level (1-9 for gzip, 0-11 for brotli)
//
// Returns:
// - io.WriteCloser: The compression writer
// - error: Any error that occurred during creation
NewWriter(w io.Writer, level int) (io.WriteCloser, error)
// GetWriter returns a pooled compression writer for the given level and output.
//
// This method reuses compression writers from a pool to minimize
// allocations and improve performance. The returned CompressionWriter
// includes a Put function that should be called to return the writer
// to the pool.
//
// Args:
// - level: The compression level (1-9 for gzip, 0-11 for brotli)
// - w: The output writer to compress to
//
// Returns:
// - *CompressionWriter: Pooled compression writer with cleanup function
// - error: Any error that occurred during creation
GetWriter(level int, w io.Writer) (*CompressionWriter, error)
// CompressionType returns the type of compression this provider implements.
//
// This method returns the CompressionType constant that identifies
// the compression algorithm (e.g., GzipCompression, BrotliCompression).
CompressionType() CompressionType
// IsSupported returns whether this compression type is supported.
//
// This method checks if the compression algorithm is available
// on the current system. Gzip is always supported (stdlib),
// while brotli requires the external package to be available.
//
// Returns:
// - bool: true if the compression type is supported, false otherwise
IsSupported() bool
}
// ResponseWriter wraps http.ResponseWriter to provide compression.
//
// This struct implements http.ResponseWriter and adds compression capabilities.
// It automatically handles:
// - Compression decision based on Accept-Encoding header
// - Compression level and minimum size thresholds
// - Proper HTTP headers (Content-Encoding, Vary)
// - Writer pooling for performance
// - Automatic cleanup via defer
//
// The compression is applied lazily - the compression writer is only
// created when the first Write call is made, avoiding unnecessary
// setup for responses that might be empty.
//
// Example:
//
// // In middleware
// crw := &compression.ResponseWriter{
// rw: c.ResponseWriter(),
// provider: compression.NewGzipProvider(),
// level: 6,
// minSize: 1024,
// }
// c.SetResponseWriter(crw)
// defer crw.Close() // Ensure cleanup
//
// // Continue with handler - compression happens automatically
// return next(c)
type ResponseWriter struct {
rw http.ResponseWriter
provider CompressionProvider
level int
minSize int
writer io.WriteCloser
put func()
wroteHeader bool
useCompression bool
contentLength int
}
// Header returns the underlying response headers map.
//
// This method provides access to the response headers for setting
// custom headers before compression is applied.
//
// Example:
//
// crw := &compression.ResponseWriter{...}
// crw.Header().Set("Content-Type", "application/json")
// crw.Header().Set("Cache-Control", "public, max-age=3600")
func (r *ResponseWriter) Header() http.Header { return r.rw.Header() }
// WriteHeader writes the HTTP response header and decides whether to use compression.
//
// This method is called automatically when the first Write call is made,
// or can be called explicitly to control when headers are written.
//
// Compression decision logic:
// - Skips compression for HEAD requests (no body to compress)
// - Skips compression for 204 No Content and 304 Not Modified responses
// - Skips compression for 1xx informational responses
// - Skips compression if Content-Encoding is already set
// - Skips compression if the provider is not supported
// - Sets appropriate headers (Content-Encoding, Vary) if compressing
//
// Example:
//
// crw := &compression.ResponseWriter{...}
// crw.Header().Set("Content-Type", "application/json")
// crw.WriteHeader(http.StatusOK) // Headers written, compression decided
func (r *ResponseWriter) WriteHeader(status int) {
if r.wroteHeader {
return
}
r.wroteHeader = true
// Skip compression for informational responses (1xx)
if status < 200 {
r.useCompression = false
r.rw.WriteHeader(status)
return
}
// Skip compression for certain status codes that shouldn't have a body
if status == http.StatusNoContent || status == http.StatusNotModified {
r.useCompression = false
r.rw.WriteHeader(status)
return
}
// Check if content is already encoded
enc := r.Header().Get("Content-Encoding")
if enc != "" && enc != "identity" {
r.useCompression = false
r.rw.WriteHeader(status)
return
}
// Only set compression headers if the provider is supported
if r.provider != nil && r.provider.IsSupported() {
r.useCompression = true
r.Header().Del("Content-Length")
r.Header().Set("Content-Encoding", string(r.provider.CompressionType()))
r.Header().Add("Vary", "Accept-Encoding")
} else {
r.useCompression = false
}
r.rw.WriteHeader(status)
}
// Write writes data to the response, compressing if enabled.
//
// This method handles the compression logic:
// - Automatically calls WriteHeader if not already called
// - Tracks content length for minimum size threshold
// - Disables compression if response becomes too small
// - Creates compression writer lazily on first write
// - Handles compression errors gracefully
//
// The compression writer is created lazily to avoid unnecessary
// setup for responses that might be empty or very small.
//
// Example:
//
// crw := &compression.ResponseWriter{...}
// data := []byte("Hello, World!")
// _, err := crw.Write(data) // Compression happens automatically
// if err != nil {
// return err
// }
func (r *ResponseWriter) Write(p []byte) (int, error) {
if !r.wroteHeader {
r.WriteHeader(http.StatusOK)
}
r.contentLength += len(p)
// Don't compress if response is too small
if r.useCompression && r.contentLength < r.minSize {
r.useCompression = false
// Remove compression headers if we're not compressing
r.Header().Del("Content-Encoding")
r.Header().Del("Vary")
}
if !r.useCompression {
return r.rw.Write(p)
}
if r.writer == nil {
compWriter, err := r.provider.GetWriter(r.level, r.rw)
if err != nil {
// Fallback to no compression on error
r.useCompression = false
r.Header().Del("Content-Encoding")
r.Header().Del("Vary")
return r.rw.Write(p)
}
r.writer = compWriter.Writer
r.put = compWriter.Put
}
return r.writer.Write(p)
}
// Close ensures proper cleanup of the compression writer.
//
// This method should be called via defer in middleware to ensure
// proper cleanup and prevent memory leaks. It handles:
// - Returning the writer to the pool (if pooled)
// - Closing the compression writer
// - Cleaning up internal state
//
// Example:
//
// // In middleware
// crw := &compression.ResponseWriter{...}
// c.SetResponseWriter(crw)
// defer crw.Close() // Always call this
//
// return next(c)
func (r *ResponseWriter) Close() error {
if r.writer != nil {
if r.put != nil {
r.put()
r.writer, r.put = nil, nil
return nil
}
return r.writer.Close()
}
return nil
}
// Flush flushes the compression writer and forwards to the underlying ResponseWriter.
//
// This method is useful for streaming responses or when immediate
// data transmission is needed. It flushes both the compression
// writer (if it supports flushing) and the underlying ResponseWriter.
//
// Example:
//
// crw := &compression.ResponseWriter{...}
// crw.Write([]byte("partial data"))
// crw.Flush() // Ensure data is sent immediately
// crw.Write([]byte("more data"))
func (r *ResponseWriter) Flush() {
if r.writer != nil {
if f, ok := r.writer.(interface{ Flush() error }); ok {
_ = f.Flush()
}
}
if f, ok := r.rw.(http.Flusher); ok {
f.Flush()
}
}
// Ensure interfaces are implemented
var _ http.ResponseWriter = (*ResponseWriter)(nil)
var _ http.Flusher = (*ResponseWriter)(nil)
// Global compression pools for reuse across different compression types
var compressionPools sync.Map // map[string]*sync.Pool
// getPool returns a sync.Pool for the given compression type and level.
//
// This function manages global compression writer pools to minimize
// allocations and improve performance. Each compression type and level
// combination gets its own pool.
//
// Args:
// - compType: The compression type (gzip, brotli, etc.)
// - level: The compression level
//
// Returns:
// - *sync.Pool: The pool for the given type and level
//
// Example:
//
// // Get a pool for gzip level 6
// pool := getPool(compression.GzipCompression, 6)
//
// // Get a pool for brotli level 11
// pool := getPool(compression.BrotliCompression, 11)
func getPool(compType CompressionType, level int) *sync.Pool {
key := string(compType) + ":" + string(rune(level))
poolAny, _ := compressionPools.LoadOrStore(key, &sync.Pool{})
return poolAny.(*sync.Pool)
}