mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat: add configurable index fallback for static files (#534)
* feat(staticfile): add configurable index fallback for SPAs - Fixes #https://github.com/railwayapp/railpack/issues/533 - Introduce index_fallback option in Staticfile config to toggle SPA routing. - Improve root directory detection by trimming whitespace from environment variables and configuration. * feat: add index fallback example for static files - Create a new example directory demonstrating the index_fallback configuration. - Include a test case to verify that non-existent routes correctly serve index.html. Generated-by: aiautocommit * test: update snapshots Generated-by: aiautocommit * test: update static file config and index test cases Generated-by: aiautocommit * test: add SPA index fallback verification to test suite Generated-by: aiautocommit * docs: document RAILPACK_VERBOSE and clarify Staticfile config Generated-by: aiautocommit
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"assets": {
|
||||
"Caddyfile": "{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges\n\t}\n}\n\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Enable cross-site filter (XSS) and tell browsers to block detected attacks\n\t\tX-XSS-Protection \"1; mode=block\"\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Keep referrer data off of HTTP connections\n\t\tReferrer-Policy \"strict-origin-when-cross-origin\"\n\t\t# Enable strict Content Security Policy\n\t\tContent-Security-Policy \"default-src 'self'; img-src 'self' data: https: *; style-src 'self' 'unsafe-inline' https: *; script-src 'self' 'unsafe-inline' https: *; font-src 'self' data: https: *; connect-src 'self' https: *; media-src 'self' https: *; object-src 'none'; frame-src 'self' https: *;\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\troot * hello\n\n\t# Handle static files\n\tfile_server {\n\t\thide .git\n\t\thide .env*\n\t}\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# Try files with HTML extension and handle SPA routing\n\ttry_files {path} {path}.html {path}/index.html /index.html\n\n\t# Handle 404 errors\n\thandle_errors {\n\t\trewrite * /{err.status_code}.html\n\t\tfile_server\n\t}\n}\n"
|
||||
"Caddyfile": "{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges\n\t}\n}\n\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Enable cross-site filter (XSS) and tell browsers to block detected attacks\n\t\tX-XSS-Protection \"1; mode=block\"\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Keep referrer data off of HTTP connections\n\t\tReferrer-Policy \"strict-origin-when-cross-origin\"\n\t\t# Enable strict Content Security Policy\n\t\tContent-Security-Policy \"default-src 'self'; img-src 'self' data: https: *; style-src 'self' 'unsafe-inline' https: *; script-src 'self' 'unsafe-inline' https: *; font-src 'self' data: https: *; connect-src 'self' https: *; media-src 'self' https: *; object-src 'none'; frame-src 'self' https: *;\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\troot * hello\n\n\t# Handle static files\n\tfile_server {\n\t\thide .git\n\t\thide .env*\n\t}\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# Try files with HTML extension\n\ttry_files {path} {path}.html {path}/index.html\n\n\thandle_errors {\n\t\trewrite * /{err.status_code}.html\n\t\tfile_server\n\t}\n}\n"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"deploy": {
|
||||
"base": {
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.3.16"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/railpack/caddy"
|
||||
],
|
||||
"step": "packages:caddy"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "caddy run --config Caddyfile --adapter caddyfile 2\u003e\u00261"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "mise install-into [email protected] /railpack/caddy"
|
||||
},
|
||||
{
|
||||
"path": "/railpack/caddy"
|
||||
},
|
||||
{
|
||||
"path": "/railpack/caddy/bin"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.3.16"
|
||||
}
|
||||
],
|
||||
"name": "packages:caddy",
|
||||
"variables": {
|
||||
"MISE_PARANOID": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assets": {
|
||||
"Caddyfile": "{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges\n\t}\n}\n\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Enable cross-site filter (XSS) and tell browsers to block detected attacks\n\t\tX-XSS-Protection \"1; mode=block\"\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Keep referrer data off of HTTP connections\n\t\tReferrer-Policy \"strict-origin-when-cross-origin\"\n\t\t# Enable strict Content Security Policy\n\t\tContent-Security-Policy \"default-src 'self'; img-src 'self' data: https: *; style-src 'self' 'unsafe-inline' https: *; script-src 'self' 'unsafe-inline' https: *; font-src 'self' data: https: *; connect-src 'self' https: *; media-src 'self' https: *; object-src 'none'; frame-src 'self' https: *;\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\troot * .\n\n\t# Handle static files\n\tfile_server {\n\t\thide .git\n\t\thide .env*\n\t}\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# Try files with HTML extension and handle SPA routing\n\ttry_files {path} {path}.html {path}/index.html /index.html\n\n\thandle_errors {\n\t\trewrite * /{err.status_code}.html\n\t\tfile_server\n\t}\n}\n"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"name": "Caddyfile",
|
||||
"path": "Caddyfile"
|
||||
},
|
||||
{
|
||||
"cmd": "caddy fmt --overwrite Caddyfile"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:caddy"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"assets": {
|
||||
"Caddyfile": "{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges\n\t}\n}\n\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Enable cross-site filter (XSS) and tell browsers to block detected attacks\n\t\tX-XSS-Protection \"1; mode=block\"\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Keep referrer data off of HTTP connections\n\t\tReferrer-Policy \"strict-origin-when-cross-origin\"\n\t\t# Enable strict Content Security Policy\n\t\tContent-Security-Policy \"default-src 'self'; img-src 'self' data: https: *; style-src 'self' 'unsafe-inline' https: *; script-src 'self' 'unsafe-inline' https: *; font-src 'self' data: https: *; connect-src 'self' https: *; media-src 'self' https: *; object-src 'none'; frame-src 'self' https: *;\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\troot * .\n\n\t# Handle static files\n\tfile_server {\n\t\thide .git\n\t\thide .env*\n\t}\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# Try files with HTML extension and handle SPA routing\n\ttry_files {path} {path}.html {path}/index.html /index.html\n\n\t# Handle 404 errors\n\thandle_errors {\n\t\trewrite * /{err.status_code}.html\n\t\tfile_server\n\t}\n}\n"
|
||||
"Caddyfile": "{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges\n\t}\n}\n\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Enable cross-site filter (XSS) and tell browsers to block detected attacks\n\t\tX-XSS-Protection \"1; mode=block\"\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Keep referrer data off of HTTP connections\n\t\tReferrer-Policy \"strict-origin-when-cross-origin\"\n\t\t# Enable strict Content Security Policy\n\t\tContent-Security-Policy \"default-src 'self'; img-src 'self' data: https: *; style-src 'self' 'unsafe-inline' https: *; script-src 'self' 'unsafe-inline' https: *; font-src 'self' data: https: *; connect-src 'self' https: *; media-src 'self' https: *; object-src 'none'; frame-src 'self' https: *;\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\troot * .\n\n\t# Handle static files\n\tfile_server {\n\t\thide .git\n\t\thide .env*\n\t}\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# Try files with HTML extension\n\ttry_files {path} {path}.html {path}/index.html\n\n\thandle_errors {\n\t\trewrite * /{err.status_code}.html\n\t\tfile_server\n\t}\n}\n"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
-Server
|
||||
}
|
||||
|
||||
root * {{.STATIC_FILE_ROOT}}
|
||||
root * {{.StaticFileRoot}}
|
||||
|
||||
# Handle static files
|
||||
file_server {
|
||||
@@ -47,10 +47,9 @@
|
||||
zstd
|
||||
}
|
||||
|
||||
# Try files with HTML extension and handle SPA routing
|
||||
try_files {path} {path}.html {path}/index.html /index.html
|
||||
# Try files with HTML extension{{if .IndexFallback}} and handle SPA routing{{end}}
|
||||
try_files {path} {path}.html {path}/index.html{{if .IndexFallback}} /index.html{{end}}
|
||||
|
||||
# Handle 404 errors
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
file_server
|
||||
|
||||
@@ -6,6 +6,7 @@ package staticfile
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/railwayapp/railpack/core/generate"
|
||||
"github.com/railwayapp/railpack/core/plan"
|
||||
@@ -19,32 +20,26 @@ const (
|
||||
CaddyfilePath = "Caddyfile"
|
||||
)
|
||||
|
||||
// Staticfile in a directory is parsed into this structure
|
||||
type StaticfileConfig struct {
|
||||
RootDir string `yaml:"root"`
|
||||
// IndexFallback controls whether to fall back to index.html for unmatched routes (default: true)
|
||||
IndexFallback *bool `yaml:"index_fallback"`
|
||||
}
|
||||
|
||||
type StaticfileProvider struct {
|
||||
RootDir string
|
||||
}
|
||||
type StaticfileProvider struct{}
|
||||
|
||||
func (p *StaticfileProvider) Name() string {
|
||||
return "staticfile"
|
||||
}
|
||||
|
||||
func (p *StaticfileProvider) Initialize(ctx *generate.GenerateContext) error {
|
||||
rootDir, err := getRootDir(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.RootDir = rootDir
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *StaticfileProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
|
||||
rootDir, err := getRootDir(ctx)
|
||||
if rootDir != "" && err == nil {
|
||||
_, err := getRootDir(ctx)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -52,15 +47,20 @@ func (p *StaticfileProvider) Detect(ctx *generate.GenerateContext) (bool, error)
|
||||
}
|
||||
|
||||
func (p *StaticfileProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
rootDir, err := getRootDir(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE `latest` is not used intentionally here as the Caddyfile template should be tested against major caddy upgrades
|
||||
installCaddyStep := ctx.NewInstallBinStepBuilder("packages:caddy")
|
||||
installCaddyStep.Default("caddy", "latest")
|
||||
installCaddyStep.Default("caddy", "2")
|
||||
|
||||
build := ctx.NewCommandStep("build")
|
||||
build.AddInput(plan.NewStepLayer(installCaddyStep.Name()))
|
||||
build.AddInput(ctx.NewLocalLayer())
|
||||
|
||||
err := p.addCaddyfileToStep(ctx, build)
|
||||
if err != nil {
|
||||
if err := p.addCaddyfileToStep(ctx, build, rootDir, getIndexFallback(ctx)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -79,17 +79,24 @@ func (p *StaticfileProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
func (p *StaticfileProvider) CleansePlan(buildPlan *plan.BuildPlan) {}
|
||||
|
||||
func (p *StaticfileProvider) StartCommandHelp() string {
|
||||
return ""
|
||||
return "Railpack serves static files using Caddy. To configure the static file root, Railpack will check:\n\n" +
|
||||
"1. The RAILPACK_STATIC_FILE_ROOT environment variable\n\n" +
|
||||
"2. The \"root\" field in a Staticfile in your project root:\n" +
|
||||
" root: dist\n\n" +
|
||||
"3. A \"public\" directory\n\n" +
|
||||
"4. An index.html in your project root\n\n" +
|
||||
"To enable SPA-style index.html fallback for unmatched routes, set \"index_fallback: true\" in your Staticfile."
|
||||
}
|
||||
|
||||
func (p *StaticfileProvider) addCaddyfileToStep(ctx *generate.GenerateContext, setup *generate.CommandStepBuilder) error {
|
||||
ctx.Logger.LogInfo("Using root dir: %s", p.RootDir)
|
||||
func (p *StaticfileProvider) addCaddyfileToStep(ctx *generate.GenerateContext, setup *generate.CommandStepBuilder, rootDir string, indexFallback bool) error {
|
||||
ctx.Logger.LogInfo("Using staticfile root dir: %s", rootDir)
|
||||
|
||||
data := map[string]any{
|
||||
"STATIC_FILE_ROOT": p.RootDir,
|
||||
caddyfileTemplateVariables := map[string]any{
|
||||
"StaticFileRoot": rootDir,
|
||||
"IndexFallback": indexFallback,
|
||||
}
|
||||
|
||||
caddyfileTemplate, err := ctx.TemplateFiles([]string{"Caddyfile.template", "Caddyfile"}, caddyfileTemplate, data)
|
||||
caddyfileTemplate, err := ctx.TemplateFiles([]string{"Caddyfile.template", "Caddyfile"}, caddyfileTemplate, caddyfileTemplateVariables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -110,14 +117,21 @@ func (p *StaticfileProvider) addCaddyfileToStep(ctx *generate.GenerateContext, s
|
||||
return nil
|
||||
}
|
||||
|
||||
// only returns an empty string if no root dir can be found
|
||||
func getRootDir(ctx *generate.GenerateContext) (string, error) {
|
||||
if rootDir, _ := ctx.Env.GetConfigVariable("STATIC_FILE_ROOT"); rootDir != "" {
|
||||
return rootDir, nil
|
||||
rootDir = strings.TrimSpace(rootDir)
|
||||
if rootDir != "" {
|
||||
return rootDir, nil
|
||||
}
|
||||
}
|
||||
|
||||
staticfileConfig, err := getStaticfileConfig(ctx)
|
||||
if staticfileConfig != nil && err == nil {
|
||||
return staticfileConfig.RootDir, nil
|
||||
rootDir := strings.TrimSpace(staticfileConfig.RootDir)
|
||||
if rootDir != "" {
|
||||
return rootDir, nil
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.App.HasMatch("public") {
|
||||
@@ -129,6 +143,17 @@ func getRootDir(ctx *generate.GenerateContext) (string, error) {
|
||||
return "", fmt.Errorf("no static file root dir found")
|
||||
}
|
||||
|
||||
func getIndexFallback(ctx *generate.GenerateContext) bool {
|
||||
// TODO we probably want to add a ENV var for this config option in the future
|
||||
config, err := getStaticfileConfig(ctx)
|
||||
if config != nil && err == nil && config.IndexFallback != nil {
|
||||
return *config.IndexFallback
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// convert a Staticfile in the app source into a struct that we can read options from
|
||||
func getStaticfileConfig(ctx *generate.GenerateContext) (*StaticfileConfig, error) {
|
||||
if !ctx.App.HasFile(StaticfileConfigName) {
|
||||
return nil, nil
|
||||
|
||||
@@ -58,6 +58,24 @@ func TestGetRootDir(t *testing.T) {
|
||||
want: "/custom/path",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "trimmed env var",
|
||||
path: "../../../examples/staticfile-index",
|
||||
envVars: map[string]string{
|
||||
"RAILPACK_STATIC_FILE_ROOT": " /custom/path ",
|
||||
},
|
||||
want: "/custom/path",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "blank env var falls back",
|
||||
path: "../../../examples/staticfile-index",
|
||||
envVars: map[string]string{
|
||||
"RAILPACK_STATIC_FILE_ROOT": " ",
|
||||
},
|
||||
want: ".",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "from staticfile config",
|
||||
path: "../../../examples/staticfile-config",
|
||||
|
||||
@@ -30,6 +30,7 @@ To configure more parts of the build, it is recommended to use a [config file](/
|
||||
|
||||
These environment variables affect the behavior of Railpack:
|
||||
|
||||
| Name | Description |
|
||||
| :------------ | :------------------------------------------ |
|
||||
| `FORCE_COLOR` | Force colored output even when not in a TTY |
|
||||
| Name | Description |
|
||||
| :---------------- | :------------------------------------------ |
|
||||
| `FORCE_COLOR` | Force colored output even when not in a TTY |
|
||||
| `RAILPACK_VERBOSE` | Enable verbose logging (equivalent to `--verbose` flag) |
|
||||
|
||||
@@ -29,13 +29,21 @@ The provider determines the root directory in this order:
|
||||
|
||||
### Staticfile
|
||||
|
||||
You can configure the root directory with a `Staticfile` that contains the contents:
|
||||
You can configure static file serving with a `Staticfile` in your project
|
||||
root:
|
||||
|
||||
```yaml
|
||||
# root directory to serve
|
||||
root: dist
|
||||
|
||||
# enable SPA routing: fall back to index.html for unmatched routes
|
||||
index_fallback: true
|
||||
```
|
||||
|
||||
`index_fallback` defaults to `false`. Enable it for single-page applications
|
||||
(React, Vue, Angular, etc.) where the frontend router handles all routes and
|
||||
the server should always serve `index.html` for unmatched paths.
|
||||
|
||||
### Config Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|
||||
@@ -6,5 +6,13 @@
|
||||
"expected": 200,
|
||||
"internalPort": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
// SPA index fallback: unmatched routes should return index.html so client-side router can handle them
|
||||
"httpCheck": {
|
||||
"path": "/nonexistent",
|
||||
"expected": 200,
|
||||
"internalPort": 80
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
[
|
||||
{
|
||||
"httpCheck": {
|
||||
"path": "/invalid",
|
||||
"expected": 404,
|
||||
"internalPort": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"httpCheck": {
|
||||
"internalPort": 80,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
index_fallback: true
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Railpack Static File (index fallback)</title>
|
||||
|
||||
<link rel="stylesheet" href="https://smol.jakerunzer.com/smol.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>Railpack Static File</h1>
|
||||
<p>
|
||||
This is a static file served by Railpack with index fallback enabled.
|
||||
</p>
|
||||
</header>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"httpCheck": {
|
||||
"internalPort": 80,
|
||||
"path": "/",
|
||||
"expected": 200,
|
||||
"expectedOutput": "Railpack Static File"
|
||||
}
|
||||
},
|
||||
{
|
||||
// non-existent routes should fall back to index.html
|
||||
"httpCheck": {
|
||||
"internalPort": 80,
|
||||
"path": "/nonexistent",
|
||||
"expected": 200,
|
||||
"expectedOutput": "Railpack Static File"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -9,5 +9,13 @@
|
||||
"internalPort": 80,
|
||||
"expectedOutput": "Railpack Static File"
|
||||
}
|
||||
},
|
||||
// by default SPA mode / index fallback should not be enabled
|
||||
{
|
||||
"httpCheck": {
|
||||
"path": "/invalid",
|
||||
"expected": 404,
|
||||
"internalPort": 80
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user