mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-21 16:27:46 +01:00
Error Page Middleware
Custom error page serving middleware that replaces default HTTP error responses with styled custom pages.
Overview
This package provides two components:
- errorpage package: Manages error page file loading, caching, and hot-reloading from disk
- CustomErrorPage middleware: Intercepts error responses and replaces them with custom error pages
Architecture
graph TD
A[HTTP Error Response] --> B{CustomErrorPage Middleware}
B --> C{Status Code & Content Type}
C -->|HTML/Plain| D[Look Up Error Page]
C -->|Other| E[Pass Through]
D --> F{Page Found?}
F -->|Yes| G[Replace Body<br/>Set Content-Type]
F -->|No| H[Log Error<br/>Pass Through]
G --> I[Custom Error Page Response]
subgraph Error Page Management
J[Error Pages Directory]
K[File Watcher]
L[Content Cache]
M[HTTP Handler]
end
J -->|Read| L
J -->|Watch Changes| K
K -->|Notify| L
L --> M
Error Page Lookup Flow
flowchart TD
A[Error Status: 503] --> B{Look for 503.html?}
B -->|Found| C[Return 503.html]
B -->|Not Found| D{Look for 404.html?}
D -->|Found| E[Return 404.html]
D -->|Not Found| F[Return Default Error]
Core Components
Error Page Package
var (
setupOnce sync.Once
dirWatcher watcher.Watcher
fileContentMap = xsync.NewMap[string, []byte]()
)
func setup() {
t := task.RootTask("error_page", false)
dirWatcher = watcher.NewDirectoryWatcher(t, errPagesBasePath)
loadContent()
go watchDir()
}
// GetStaticFile retrieves an error page file by filename
func GetStaticFile(filename string) ([]byte, bool)
// GetErrorPageByStatus retrieves the error page for a given status code
func GetErrorPageByStatus(statusCode int) (content []byte, ok bool)
File Watcher
The package watches the error pages directory for changes:
func watchDir() {
eventCh, errCh := dirWatcher.Events(task.RootContext())
for {
select {
case event := <-eventCh:
filename := event.ActorName
switch event.Action {
case events.ActionFileWritten:
fileContentMap.Delete(filename)
loadContent()
case events.ActionFileDeleted:
fileContentMap.Delete(filename)
case events.ActionFileRenamed:
fileContentMap.Delete(filename)
loadContent()
}
case err := <-errCh:
gperr.LogError("error watching error page directory", err)
}
}
}
Custom Error Page Middleware
type customErrorPage struct{}
var CustomErrorPage = NewMiddleware[customErrorPage]()
const StaticFilePathPrefix = "/$gperrorpage/"
Request Modifier
func (customErrorPage) before(w http.ResponseWriter, r *http.Request) bool {
return !ServeStaticErrorPageFile(w, r)
}
Response Modifier
func (customErrorPage) modifyResponse(resp *http.Response) error {
// Only handles:
// - Non-success status codes (4xx, 5xx)
// - HTML or Plain Text content types
contentType := httputils.GetContentType(resp.Header)
if !httputils.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
errorPage, ok := errorpage.GetErrorPageByStatus(resp.StatusCode)
if ok {
// Replace response body with error page
resp.Body = io.NopCloser(bytes.NewReader(errorPage))
resp.ContentLength = int64(len(errorPage))
resp.Header.Set(httpheaders.HeaderContentLength, strconv.Itoa(len(errorPage)))
resp.Header.Set(httpheaders.HeaderContentType, "text/html; charset=utf-8")
}
}
return nil
}
Static File Serving
The middleware also serves static error page assets:
func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) bool {
if strings.HasPrefix(path, StaticFilePathPrefix) {
filename := path[len(StaticFilePathPrefix):]
file, ok := errorpage.GetStaticFile(filename)
if ok {
// Set content type based on extension
switch ext := filepath.Ext(filename); ext {
case ".html":
w.Header().Set(httpheaders.HeaderContentType, "text/html; charset=utf-8")
case ".js":
w.Header().Set(httpheaders.HeaderContentType, "application/javascript; charset=utf-8")
case ".css":
w.Header().Set(httpheaders.HeaderContentType, "text/css; charset=utf-8")
}
w.Write(file)
return true
}
}
return false
}
Configuration
Error Pages Directory
Default path: config/error_pages/
Supported Files
| File Pattern | Description |
|---|---|
{statusCode}.html |
Specific error page (e.g., 503.html) |
404.html |
Fallback for missing specific pages |
*.css |
Stylesheets |
*.js |
JavaScript files |
*.{png,jpg,svg} |
Images and assets |
Example Structure
config/error_pages/
├── 403.html
├── 404.html
├── 500.html
├── 502.html
├── 503.html
├── style.css
└── logo.png
Middleware Configuration
# In route middleware configuration
- use: errorpage
# Optional: bypass rules
bypass:
- type: PathPrefix
value: /api
Response Processing
flowchart TD
A[Backend Response] --> B{Status Code >= 400?}
B -->|No| C[Pass Through]
B -->|Yes| D{Content Type HTML/Plain?}
D -->|No| C
D -->|Yes| E{Look Up Error Page}
E -->|Found| F[Replace Body]
E -->|Not Found| G[Log Error]
G --> C
F --> H[Set Content-Type: text/html]
H --> I[Return Custom Error Page]
Usage Examples
Creating Custom Error Pages
503.html:
<!DOCTYPE html>
<html>
<head>
<title>Service Unavailable</title>
<link rel="stylesheet" href="/$gperrorpage/style.css">
</head>
<body>
<div class="error-container">
<h1>503 - Service Unavailable</h1>
<p>The service is temporarily unavailable. Please try again later.</p>
</div>
</body>
</html>
Using in Middleware Chain
# config/middlewares/error-pages.yml
- use: errorpage
bypass:
- type: PathPrefix
value: /api/health
Programmatic Usage
import (
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
)
// Check if error page exists
content, ok := errorpage.GetErrorPageByStatus(503)
if ok {
// Use error page content
}
// Serve static asset
ServeStaticErrorPageFile(w, r)
Integration with GoDoxy
The error page middleware integrates with:
- File Watching: Uses
internal/watcherfor hot-reloading - Task Management: Uses
internal/taskfor lifetime management - Content Caching: Uses
xsync.Mapfor thread-safe caching - HTTP Headers: Uses
goutils/http/httpheadersfor content type handling
Performance Considerations
- Error page content is cached in memory after first load
- File watcher notifies on changes for cache invalidation
- Static files are served directly from cache
- Concurrent access protected by
xsync.Map
Error Handling
// Logging on error page not found
log.Error().Msgf("unable to load error page for status %d", resp.StatusCode)
// Logging on static file not found
log.Error().Msg("unable to load resource " + filename)