Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions infra/caddy/Caddyfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@ oullin.io {
format json
}

# --- Writing archive: every legacy post has an explicit canonical target
redir /post/2026-02-19-local-first-skills-platform-for-ai-agents https://writing.gocanto.sh/local-first-skills-platform-for-ai-agents 301
redir /post/2023-06-21-bugs-are-inevitable-in-software-development https://writing.gocanto.sh/bugs-are-inevitable-in-software-development 301
redir /post/2025-09-25-shipping-seo-for-a-single-page-app-the-pragmatic-way https://writing.gocanto.sh/shipping-seo-for-a-single-page-app-the-pragmatic-way 301
redir /post/2025-10-09-when-a-real-time-feature-store-is-the-wrong-fix-for-fraud https://writing.gocanto.sh/when-a-real-time-feature-store-is-the-wrong-fix-for-fraud 301
redir /post/2025-11-04-your-ai-model-is-not-your-product https://writing.gocanto.sh/your-ai-model-is-not-your-product 301
redir /post/2026-04-05-go-meets-the-monolith-renaissance https://writing.gocanto.sh/go-meets-the-monolith-renaissance 301
redir /post/2025-12-29-nostalgia-is-a-security-risk https://writing.gocanto.sh/nostalgia-is-a-security-risk 301
redir /post/2025-11-21-demystifying-the-go-maps-engine https://writing.gocanto.sh/demystifying-the-go-maps-engine 301
redir /post/2025-12-15-money-in-Go-done-properly https://writing.gocanto.sh/money-in-go-done-properly 301
redir /post/2026-03-13-taming-complex-state-in-go https://writing.gocanto.sh/taming-complex-state-in-go 301
redir /post/2026-03-31-laravel-collections-to-go https://writing.gocanto.sh/laravel-collections-to-go 301
redir /post/2025-10-29-turbocharging-web-performance-automated-early-hints https://writing.gocanto.sh/turbocharging-web-performance-automated-early-hints 301
redir /post/2025-11-13-shipping-observability-for-oullin-infrastructure https://writing.gocanto.sh/shipping-observability-for-oullin-infrastructure 301
redir /post/2025-11-26-the-case-of-the-mismatched-clocks-lesson-secure-timing https://writing.gocanto.sh/the-case-of-the-mismatched-clocks-lesson-secure-timing 301
redir /post/2026-02-26-stop-installing-python-just-to-convert-files-to-markdown https://writing.gocanto.sh/stop-installing-python-just-to-convert-files-to-markdown 301
redir /post/2025-11-15-debugging-multi-layered-docker-deployment https://writing.gocanto.sh/debugging-multi-layered-docker-deployment 301
redir /post/2023-06-22-positive-emotions-reduce-negative-emotions https://writing.gocanto.sh/positive-emotions-reduce-negative-emotions 301
redir /post/2025-04-02-embrace-growth-through-movement https://writing.gocanto.sh/embrace-growth-through-movement 301
redir /post/2025-10-17-the-operating-system-of-you-an-engineering-leader-reflection https://writing.gocanto.sh/the-operating-system-of-you-an-engineering-leader-reflection 301
redir /post/2025-11-03-honesty-establishes-trust-enabling-process-formation-progression https://writing.gocanto.sh/honesty-establishes-trust-enabling-process-formation-progression 301
redir /post/2025-11-30-work-is-not-family-and-that-is-a-good-thing https://writing.gocanto.sh/work-is-not-family-and-that-is-a-good-thing 301
redir /post/2026-02-24-engineering-management-mistakes-we-need-to-stop-making.md https://writing.gocanto.sh/engineering-management-mistakes-we-need-to-stop-making 301
redir /post/2025-12-05-handling-money-in-php https://writing.gocanto.sh/handling-money-in-php 301
redir /post/2025-10-15-building-oullin https://writing.gocanto.sh/building-oullin 301
redir /writing https://writing.gocanto.sh/ 301
redir /writing/ https://writing.gocanto.sh/ 301
redir /tags/* https://writing.gocanto.sh/ 301

# --- Public listener: block protected paths
@protected_public path /api/generate-signature* /api/metrics
handle @protected_public {
Expand Down
74 changes: 74 additions & 0 deletions infra/caddy/caddyfile_prod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,77 @@ func TestProdCaddyfileKeepsSignatureEndpointBehindMTLS(t *testing.T) {
t.Fatal("expected /api/generate-signature* to be handled by api:8080 inside the :8443 mTLS listener")
}
}

func TestProdCaddyfileRedirectsWritingArchiveBeforeDefaultProxy(t *testing.T) {
caddyfile := stripCaddyComments(readProdCaddyfile(t))
oullinBlock, ok := caddyBlock(caddyfile, "oullin.io")
if !ok {
t.Fatal("expected production Caddyfile to contain the oullin.io site")
}

redirects := []struct {
source string
target string
}{
{"/post/2026-02-19-local-first-skills-platform-for-ai-agents", "https://writing.gocanto.sh/local-first-skills-platform-for-ai-agents"},
{"/post/2023-06-21-bugs-are-inevitable-in-software-development", "https://writing.gocanto.sh/bugs-are-inevitable-in-software-development"},
{"/post/2025-09-25-shipping-seo-for-a-single-page-app-the-pragmatic-way", "https://writing.gocanto.sh/shipping-seo-for-a-single-page-app-the-pragmatic-way"},
{"/post/2025-10-09-when-a-real-time-feature-store-is-the-wrong-fix-for-fraud", "https://writing.gocanto.sh/when-a-real-time-feature-store-is-the-wrong-fix-for-fraud"},
{"/post/2025-11-04-your-ai-model-is-not-your-product", "https://writing.gocanto.sh/your-ai-model-is-not-your-product"},
{"/post/2026-04-05-go-meets-the-monolith-renaissance", "https://writing.gocanto.sh/go-meets-the-monolith-renaissance"},
{"/post/2025-12-29-nostalgia-is-a-security-risk", "https://writing.gocanto.sh/nostalgia-is-a-security-risk"},
{"/post/2025-11-21-demystifying-the-go-maps-engine", "https://writing.gocanto.sh/demystifying-the-go-maps-engine"},
{"/post/2025-12-15-money-in-Go-done-properly", "https://writing.gocanto.sh/money-in-go-done-properly"},
{"/post/2026-03-13-taming-complex-state-in-go", "https://writing.gocanto.sh/taming-complex-state-in-go"},
{"/post/2026-03-31-laravel-collections-to-go", "https://writing.gocanto.sh/laravel-collections-to-go"},
{"/post/2025-10-29-turbocharging-web-performance-automated-early-hints", "https://writing.gocanto.sh/turbocharging-web-performance-automated-early-hints"},
{"/post/2025-11-13-shipping-observability-for-oullin-infrastructure", "https://writing.gocanto.sh/shipping-observability-for-oullin-infrastructure"},
{"/post/2025-11-26-the-case-of-the-mismatched-clocks-lesson-secure-timing", "https://writing.gocanto.sh/the-case-of-the-mismatched-clocks-lesson-secure-timing"},
{"/post/2026-02-26-stop-installing-python-just-to-convert-files-to-markdown", "https://writing.gocanto.sh/stop-installing-python-just-to-convert-files-to-markdown"},
{"/post/2025-11-15-debugging-multi-layered-docker-deployment", "https://writing.gocanto.sh/debugging-multi-layered-docker-deployment"},
{"/post/2023-06-22-positive-emotions-reduce-negative-emotions", "https://writing.gocanto.sh/positive-emotions-reduce-negative-emotions"},
{"/post/2025-04-02-embrace-growth-through-movement", "https://writing.gocanto.sh/embrace-growth-through-movement"},
{"/post/2025-10-17-the-operating-system-of-you-an-engineering-leader-reflection", "https://writing.gocanto.sh/the-operating-system-of-you-an-engineering-leader-reflection"},
{"/post/2025-11-03-honesty-establishes-trust-enabling-process-formation-progression", "https://writing.gocanto.sh/honesty-establishes-trust-enabling-process-formation-progression"},
{"/post/2025-11-30-work-is-not-family-and-that-is-a-good-thing", "https://writing.gocanto.sh/work-is-not-family-and-that-is-a-good-thing"},
{"/post/2026-02-24-engineering-management-mistakes-we-need-to-stop-making.md", "https://writing.gocanto.sh/engineering-management-mistakes-we-need-to-stop-making"},
{"/post/2025-12-05-handling-money-in-php", "https://writing.gocanto.sh/handling-money-in-php"},
{"/post/2025-10-15-building-oullin", "https://writing.gocanto.sh/building-oullin"},
}

defaultProxy := strings.Index(oullinBlock, "reverse_proxy web:80")
if defaultProxy == -1 {
t.Fatal("expected oullin.io site to contain the default web proxy")
}

for _, redirect := range redirects {
t.Run(redirect.source, func(t *testing.T) {
directive := "redir " + redirect.source + " " + redirect.target + " 301"
position := strings.Index(oullinBlock, directive)
if position == -1 {
t.Fatalf("expected exact writing redirect %q", directive)
}
if position > defaultProxy {
t.Fatalf("expected %s redirect before the default web proxy", redirect.source)
}
})
}

for _, directive := range []string{
"redir /writing https://writing.gocanto.sh/ 301",
"redir /writing/ https://writing.gocanto.sh/ 301",
"redir /tags/* https://writing.gocanto.sh/ 301",
} {
position := strings.Index(oullinBlock, directive)
if position == -1 {
t.Fatalf("expected archive redirect %q", directive)
}
if position > defaultProxy {
t.Fatalf("expected archive redirect %q before the default web proxy", directive)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if strings.Contains(oullinBlock, "redir /post/*") {
t.Fatal("must not use a broad /post/* redirect")
}
Comment on lines +220 to +222

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
git ls-files infra/caddy/caddyfile_prod_test.go
echo '---'
ast-grep outline infra/caddy/caddyfile_prod_test.go --view expanded
echo '---'
sed -n '180,250p' infra/caddy/caddyfile_prod_test.go

Repository: oullin/api

Length of output: 2793


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('infra/caddy/caddyfile_prod_test.go')
text = p.read_text()
for needle in ['redir /post/*', 'strings.Contains(oullinBlock', 'named matcher', '/post*']:
    print(f'### {needle}')
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            print(i, line)
PY

Repository: oullin/api

Length of output: 342


🏁 Script executed:

set -euo pipefail
# Inspect the caddy matcher docs snippet locally in case the repo vendors a reference or comment.
rg -n "redir /post\\*|/post/\\*|matcher" infra/caddy -S

Repository: oullin/api

Length of output: 550


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('infra/caddy/caddyfile_prod_test.go')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 205 <= i <= 235:
        print(f"{i:4d}: {line}")
PY

Repository: oullin/api

Length of output: 826


🏁 Script executed:

set -euo pipefail
# Inspect the production Caddyfile and any related redirect definitions.
git ls-files infra/caddy
echo '---'
rg -n "redir /post|`@post`|/post\\*" infra/caddy -S
echo '---'
# Show the relevant production block if present.
python3 - <<'PY'
from pathlib import Path
for path in [Path('infra/caddy/Caddyfile'), Path('infra/caddy/caddyfile_prod_test.go')]:
    if path.exists():
        print(f'## {path}')
        for i, line in enumerate(path.read_text().splitlines(), 1):
            if 1 <= i <= 260 and ('/post' in line or 'redir' in line or '`@post`' in line or 'reverse_proxy' in line):
                print(f"{i:4d}: {line}")
PY

Repository: oullin/api

Length of output: 8925


🌐 Web query:

Caddy path matcher /post* /post/* matcher docs redir shortcut directive

💡 Result:

In Caddy, path matching is exact by default [1][2]. To match a path prefix, you must append an asterisk () [1][3][2]. Understanding the difference between /post and /post/* is critical [1][3]: - /post* matches /post, /post/, /postbar, and /post/bar [1][3]. - /post/* matches /post/ and /post/bar, but does NOT match /post or /postbar [1][3]. For most use cases where you want to match a path and all its sub-paths (e.g., to redirect a directory), /post/* is generally the preferred and more predictable choice [1][3][4]. The redir directive is used to issue HTTP redirects [5][6]. It accepts an optional matcher and the target location [5][7]. Common patterns: To redirect a specific path and all its sub-paths to a new location while preserving the remaining URI: redir /post/* https://example.com/new-post{uri} To redirect everything: redir https://example.com{uri} If you are using handle_path, note that it implicitly strips the matched path prefix, which can simplify your configuration if you need to perform actions on the remaining path segment after the prefix [8]. Caddyfile directives are evaluated based on a specific order, and redir is placed very early in the request pipeline [5][6][9], meaning it will trigger before most other directives (like reverse_proxy) if it matches the request [5].

Citations:


🏁 Script executed:

set -euo pipefail
# Check whether the repository includes tests or docs that spell out the intended matcher shape.
rg -n "must not use a broad /post|broad /post|writing redirect|matcher" . -S

Repository: oullin/api

Length of output: 959


Reject broad /post matchers, not just redir /post/*. This check still misses redir /post* and equivalent named matchers, which would cover /post, /post/, and subpaths. Assert against wildcarded /post sources, not just the literal /post/*.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@infra/caddy/caddyfile_prod_test.go` around lines 220 - 222, Update the
redirect validation in the relevant test to reject any wildcarded /post source,
including /post* and equivalent named matchers, rather than checking only the
literal “redir /post/*” string. Preserve acceptance of non-broad /post routes
while ensuring /post, /post/, and subpaths cannot be covered by a wildcard
matcher.

Source: MCP tools

}
Loading