Files
godoxy/internal/net/gphttp/middleware/errorpage
yusing 1244af0e38 refactor(watcher): rename events package to watcherEvents
- Updated import paths and references from events to watcherEvents across multiple files
2026-02-15 14:35:23 +08: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:

  1. errorpage package: Manages error page file loading, caching, and hot-reloading from disk
  2. 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/watcher for hot-reloading
  • Task Management: Uses internal/task for lifetime management
  • Content Caching: Uses xsync.Map for thread-safe caching
  • HTTP Headers: Uses goutils/http/httpheaders for 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)