mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-19 07:54:48 +01:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
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)