From caa2895f52158254e1cdfe2daea989527b3f6237 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 20 Oct 2026 22:28:23 +0000 Subject: [PATCH] Check validity of URL rewrite rules for non-index projects. In commit 600c8a78484c93030f79a4fa4a8bc8ca603296f1, we have introduced an unintentionally breaking change to how redirects are handled, thus breaking every single-page application deployed to a non-index project. This cannot be easily fixed due to Codeberg Pages already having a mix of broken and fixed redirects in manifests without any way to tell them apart, but we can at least emit a diagnostic for redirects that are statically known to be broken at deploy time. See also #273. --- src/manifest.go | 6 ++++-- src/redirects.go | 15 ++++++++++++--- src/update.go | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/manifest.go b/src/manifest.go index 03f9343..1464bdc 100644 --- a/src/manifest.go +++ b/src/manifest.go @@ -338,9 +338,11 @@ func CompressFiles(ctx context.Context, manifest *Manifest) { // Apply post-processing steps to the manifest. // At the moment, there isn't a good way to report errors except to log them on the terminal. // (Perhaps in the future they could be exposed at `.git-pages/status.txt`?) -func PrepareManifest(ctx context.Context, manifest *Manifest) error { +func PrepareManifest(ctx context.Context, webRoot string, manifest *Manifest) error { + _, project, _ := strings.Cut(webRoot, "/") + // Parse Netlify-style `_redirects`. - if err := ProcessRedirectsFile(ctx, manifest); err != nil { + if err := ProcessRedirectsFile(ctx, project, manifest); err != nil { logc.Printf(ctx, "redirects err: %s\n", err) } else if len(manifest.Redirects) > 0 { logc.Printf(ctx, "redirects ok: %d rules\n", len(manifest.Redirects)) diff --git a/src/redirects.go b/src/redirects.go index ca420df..be48f5e 100644 --- a/src/redirects.go +++ b/src/redirects.go @@ -43,7 +43,7 @@ func Is3xxHTTPStatus(status int) bool { return status >= 300 && status <= 399 } -func validateRedirectRule(rule *redirects.Rule) error { +func validateRedirectRule(project string, rule *redirects.Rule) error { if len(rule.Params) > 0 { return fmt.Errorf("rules with parameters are not supported") } @@ -76,11 +76,20 @@ func validateRedirectRule(rule *redirects.Rule) error { return fmt.Errorf("'to' URL may only include a hostname for 3xx status rules") } } + // After v0.9.1, we've changed the redirect handling so that the 'to' URL for a rewrite rule + // must be a valid URL pointing to this site, rather than a valid path within the manifest + // (which was the old, incorrect interpretation). This broke every SPA on git-pages that was + // deployed to a non-index project. Unfortunately, we can't do much more than diagnosing + // the problematic pattern at deploy time. + if rule.Status == http.StatusOK && project != ".index" && toURL.Host == "" && + !strings.HasPrefix(toURL.Path, fmt.Sprintf("/%s/", project)) { + return fmt.Errorf("'to' URL must start with '/%s/' for a 200 status rule", project) + } return nil } // Parses redirects file and injects rules into the manifest. -func ProcessRedirectsFile(ctx context.Context, manifest *Manifest) error { +func ProcessRedirectsFile(ctx context.Context, project string, manifest *Manifest) error { redirectsEntry := manifest.Contents[RedirectsFileName] delete(manifest.Contents, RedirectsFileName) if redirectsEntry == nil { @@ -103,7 +112,7 @@ func ProcessRedirectsFile(ctx context.Context, manifest *Manifest) error { } for index, rule := range rules { - if err := validateRedirectRule(&rule); err != nil { + if err := validateRedirectRule(project, &rule); err != nil { ruleString := redirects.Must(redirects.UnparseString([]redirects.Rule{rule})) AddProblem(manifest, RedirectsFileName, "rule #%d %q: %s", index+1, strings.TrimSpace(ruleString), err) diff --git a/src/update.go b/src/update.go index 6f18a76..9024e7c 100644 --- a/src/update.go +++ b/src/update.go @@ -70,7 +70,7 @@ func Update( outcome = UpdateDeleted } } - } else if err = PrepareManifest(ctx, newManifest); err == nil { + } else if err = PrepareManifest(ctx, webRoot, newManifest); err == nil { storedManifest, err = StoreManifest(ctx, webRoot, newManifest, opts) if err == nil { domain, _, _ := strings.Cut(webRoot, "/")