mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-08 04:45:43 +09:00
[feat](errorpages): add custom templates for HTTP errors and assets
- Implemented custom HTML templates for `400`, `404`, `500`, and `525` error pages with multilingual support. - Added embedded file system for error page templates and assets. - Introduced fallback mechanism to serve minimal plain text for missing error templates. - Integrated TailwindCSS for styling error pages, with a build script in `package.json`.
This commit is contained in:
2
internal/errorpages/assets/errors.css
Normal file
2
internal/errorpages/assets/errors.css
Normal file
@@ -0,0 +1,2 @@
|
||||
/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.static{position:static}.contents{display:contents}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.min-h-screen{min-height:100vh}.w-auto{width:auto}.w-full{width:100%}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-center{justify-content:center}.text-center{text-align:center}.tracking-\[0\.25em\]{--tw-tracking:.25em;letter-spacing:.25em}.uppercase{text-transform:uppercase}.opacity-90{opacity:.9}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
||||
BIN
internal/errorpages/assets/hop-gate.png
Normal file
BIN
internal/errorpages/assets/hop-gate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 499 KiB |
85
internal/errorpages/errorpages.go
Normal file
85
internal/errorpages/errorpages.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package errorpages
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusTLSHandshakeFailed is an HTTP-style status code representing
|
||||
// a TLS/DTLS handshake failure (similar to Cloudflare 525).
|
||||
// TLS/DTLS 핸드셰이크 실패를 나타내는 HTTP 스타일 상태 코드입니다. (예: 525)
|
||||
const StatusTLSHandshakeFailed = 525
|
||||
|
||||
//go:embed templates/*.html
|
||||
var embeddedTemplatesFS embed.FS
|
||||
|
||||
// AssetsFS embeds static assets (CSS, logos, etc.) for error pages.
|
||||
// 에러 페이지용 정적 에셋(CSS, 로고 등)을 바이너리에 포함하는 embed FS 입니다.
|
||||
//
|
||||
// Expected files (by convention):
|
||||
// - assets/errors.css
|
||||
// - assets/logo.svg
|
||||
//go:embed assets/*
|
||||
var AssetsFS embed.FS
|
||||
|
||||
// Render writes an error page HTML for the given HTTP status code to the response writer.
|
||||
// If no matching template is found, it falls back to a minimal plain text response.
|
||||
//
|
||||
// 주어진 HTTP 상태 코드에 대한 에러 페이지 HTML을 응답에 씁니다.
|
||||
// 해당 템플릿이 없으면 최소한의 텍스트 응답으로 폴백합니다.
|
||||
func Render(w http.ResponseWriter, r *http.Request, status int) {
|
||||
html, ok := Load(status)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
|
||||
if !ok {
|
||||
// Fallback to a minimal plain text response if no template is available.
|
||||
// 템플릿이 없으면 간단한 텍스트 응답으로 대체합니다.
|
||||
_, _ = fmt.Fprintf(w, "%d %s", status, http.StatusText(status))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write(html)
|
||||
}
|
||||
|
||||
// Load attempts to load an error page for the given HTTP status code.
|
||||
//
|
||||
// Priority:
|
||||
// 1. $HOP_ERROR_PAGES_DIR/<status>.html (or ./errors/<status>.html if env is empty)
|
||||
// 2. embedded template: templates/<status>.html
|
||||
//
|
||||
// 주어진 HTTP 상태 코드에 대한 에러 페이지를 로드합니다.
|
||||
//
|
||||
// 우선순위:
|
||||
// 1. $HOP_ERROR_PAGES_DIR/<status>.html (env 미설정 시 ./errors/<status>.html)
|
||||
// 2. 내장 템플릿: templates/<status>.html
|
||||
func Load(status int) ([]byte, bool) {
|
||||
name := fmt.Sprintf("%d.html", status)
|
||||
|
||||
// 1. External directory override (HOP_ERROR_PAGES_DIR, default "./errors").
|
||||
// 1. 외부 디렉터리 우선 (HOP_ERROR_PAGES_DIR, 기본값 "./errors").
|
||||
dir := strings.TrimSpace(os.Getenv("HOP_ERROR_PAGES_DIR"))
|
||||
if dir == "" {
|
||||
dir = "./errors"
|
||||
}
|
||||
if dir != "" {
|
||||
p := filepath.Join(dir, name)
|
||||
if data, err := os.ReadFile(p); err == nil {
|
||||
return data, true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Embedded default templates.
|
||||
// 2. 내장 기본 템플릿.
|
||||
p := filepath.Join("templates", name)
|
||||
if data, err := embeddedTemplatesFS.ReadFile(p); err == nil {
|
||||
return data, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
33
internal/errorpages/templates/400.html
Normal file
33
internal/errorpages/templates/400.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>400 Bad Request - HopGate</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Tailwind build output should be bundled into this file -->
|
||||
<link rel="stylesheet" href="/__hopgate_assets__/errors.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
|
||||
<div class="w-full max-w-xl text-center">
|
||||
<div class="inline-flex items-center gap-3 mb-8">
|
||||
<img src="/__hopgate_assets__/hop-gate.png" alt="HopGate" class="h-8 w-auto opacity-90" />
|
||||
<span class="text-sm font-medium tracking-[0.25em] uppercase text-slate-400">HopGate</span>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex items-baseline gap-4 mb-4">
|
||||
<span class="text-6xl md:text-7xl font-extrabold tracking-[0.25em] text-amber-300">400</span>
|
||||
<span class="text-lg md:text-xl font-semibold text-slate-100">Bad Request</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm md:text-base text-slate-300 leading-relaxed">
|
||||
The request was malformed or invalid.<br>
|
||||
요청 형식이 올바르지 않거나 지원되지 않습니다.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 text-xs md:text-sm text-slate-500">
|
||||
If you typed the URL directly, please check it for spelling.<br>
|
||||
URL을 직접 입력하셨다면 철자를 다시 한 번 확인해 주세요.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
32
internal/errorpages/templates/404.html
Normal file
32
internal/errorpages/templates/404.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>404 Not Found - HopGate</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/__hopgate_assets__/errors.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
|
||||
<div class="w-full max-w-xl text-center">
|
||||
<div class="inline-flex items-center gap-3 mb-8">
|
||||
<img src="/__hopgate_assets__/hop-gate.png" alt="HopGate" class="h-8 w-auto opacity-90" />
|
||||
<span class="text-sm font-medium tracking-[0.25em] uppercase text-slate-400">HopGate</span>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex items-baseline gap-4 mb-4">
|
||||
<span class="text-6xl md:text-7xl font-extrabold tracking-[0.25em] text-sky-400">404</span>
|
||||
<span class="text-lg md:text-xl font-semibold text-slate-100">Domain or page not found</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm md:text-base text-slate-300 leading-relaxed">
|
||||
The requested domain or path does not exist on this HopGate edge.<br>
|
||||
요청하신 도메인 또는 경로를 찾을 수 없습니다.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 text-xs md:text-sm text-slate-500">
|
||||
Please check the URL or try again later.<br>
|
||||
URL을 다시 확인하시거나 잠시 후 다시 시도해 주세요.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
32
internal/errorpages/templates/500.html
Normal file
32
internal/errorpages/templates/500.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>500 Internal Server Error - HopGate</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/__hopgate_assets__/errors.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
|
||||
<div class="w-full max-w-xl text-center">
|
||||
<div class="inline-flex items-center gap-3 mb-8">
|
||||
<img src="/__hopgate_assets__/hop-gate.png" alt="HopGate" class="h-8 w-auto opacity-90" />
|
||||
<span class="text-sm font-medium tracking-[0.25em] uppercase text-slate-400">HopGate</span>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex items-baseline gap-4 mb-4">
|
||||
<span class="text-6xl md:text-7xl font-extrabold tracking-[0.25em] text-rose-400">500</span>
|
||||
<span class="text-lg md:text-xl font-semibold text-slate-100">Internal Server Error</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm md:text-base text-slate-300 leading-relaxed">
|
||||
Something went wrong while processing your request.<br>
|
||||
요청을 처리하는 중 서버 내부 오류가 발생했습니다.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 text-xs md:text-sm text-slate-500">
|
||||
The issue has been logged and may be investigated by the operator.<br>
|
||||
이 문제는 로그로 기록되었으며, 운영자가 확인할 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
32
internal/errorpages/templates/525.html
Normal file
32
internal/errorpages/templates/525.html
Normal file
@@ -0,0 +1,32 @@
|
||||
ㄱ<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>525 TLS Handshake Failed - HopGate</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/__hopgate_assets__/errors.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
|
||||
<div class="w-full max-w-xl text-center">
|
||||
<div class="inline-flex items-center gap-3 mb-8">
|
||||
<img src="/__hopgate_assets__/hop-gate.png" alt="HopGate" class="h-8 w-auto opacity-90" />
|
||||
<span class="text-sm font-medium tracking-[0.25em] uppercase text-slate-400">HopGate</span>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex items-baseline gap-4 mb-4">
|
||||
<span class="text-6xl md:text-7xl font-extrabold tracking-[0.25em] text-amber-300">525</span>
|
||||
<span class="text-lg md:text-xl font-semibold text-slate-100">TLS/DTLS Handshake Failed</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm md:text-base text-slate-300 leading-relaxed">
|
||||
The secure tunnel to the backend could not be established.<br>
|
||||
백엔드와의 TLS/DTLS 핸드셰이크에 실패하여 요청을 처리할 수 없습니다.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 text-xs md:text-sm text-slate-500">
|
||||
This is usually a temporary issue between the edge and the origin service.<br>
|
||||
보통 엣지와 백엔드 서비스 간의 일시적인 문제일 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user