[{"data":1,"prerenderedAt":366},["ShallowReactive",2],{"\u002Fblog\u002Fcaching-preflight-requests-with-caddy":3},{"id":4,"title":5,"body":6,"description":30,"extension":354,"meta":355,"navigation":62,"path":356,"publishedAt":357,"seo":358,"stem":359,"summary":360,"tags":361,"__hash__":365},"blog\u002Fblog\u002F20241115.md","Caching preflight requests with Caddy",{"type":7,"value":8,"toc":350},"minimark",[9,14,18,21,24,212,215,257,261,264,267,274,277,343,346],[10,11,13],"h2",{"id":12},"context","Context",[15,16,17],"p",{},"Recently, I've been dabbling with PocketBase as a backend solution, and I must say it's a fantastic tool to have on your belt.\nFor a small web app with mostly CRUDy logic it's just insanely productive - you can expose a complete REST(ish) API in a matter of minutes.",[15,19,20],{},"Deploying PocketBase services is also stupid simple. Just ship the binary to a VM, write a simple systemd unit, start it, and you're done.\nIt even comes with SSL out of the box, thanks to Caddy.",[15,22,23],{},"On my server I'm using the following config:",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light","[Unit]\nDescription = service_name\n\n[Service]\nType           = simple\nUser           = malick\nGroup          = malick\nLimitNOFILE    = 4096\nRestart        = always\nRestartSec     = 5s\nStandardOutput = append:\u002Fhome\u002Fmalick\u002Fservice_name\u002Ferrors.log\nStandardError  = append:\u002Fhome\u002Fmalick\u002Fservice_name\u002Ferrors.log\nExecStart      = \u002Fhome\u002Fmalick\u002Fservice_name\u002Fpocketbase serve --http 127.0.0.1:8090 --origins \"https:\u002F\u002Fspa.com\"\n\n[Install]\nWantedBy = multi-user.target\n","bash","",[32,33,34,43,57,64,70,82,93,104,117,129,141,152,163,190,195,201],"code",{"__ignoreMap":30},[35,36,39],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sgsFI","[Unit]\n",[35,44,46,50,54],{"class":37,"line":45},2,[35,47,49],{"class":48},"s7eDp","Description",[35,51,53],{"class":52},"sYBdl"," =",[35,55,56],{"class":52}," service_name\n",[35,58,60],{"class":37,"line":59},3,[35,61,63],{"emptyLinePlaceholder":62},true,"\n",[35,65,67],{"class":37,"line":66},4,[35,68,69],{"class":41},"[Service]\n",[35,71,73,76,79],{"class":37,"line":72},5,[35,74,75],{"class":48},"Type",[35,77,78],{"class":52},"           =",[35,80,81],{"class":52}," simple\n",[35,83,85,88,90],{"class":37,"line":84},6,[35,86,87],{"class":48},"User",[35,89,78],{"class":52},[35,91,92],{"class":52}," malick\n",[35,94,96,99,102],{"class":37,"line":95},7,[35,97,98],{"class":48},"Group",[35,100,101],{"class":52},"          =",[35,103,92],{"class":52},[35,105,107,110,113],{"class":37,"line":106},8,[35,108,109],{"class":48},"LimitNOFILE",[35,111,112],{"class":52},"    =",[35,114,116],{"class":115},"sYu0t"," 4096\n",[35,118,120,123,126],{"class":37,"line":119},9,[35,121,122],{"class":48},"Restart",[35,124,125],{"class":52},"        =",[35,127,128],{"class":52}," always\n",[35,130,132,135,138],{"class":37,"line":131},10,[35,133,134],{"class":48},"RestartSec",[35,136,137],{"class":52},"     =",[35,139,140],{"class":52}," 5s\n",[35,142,144,147,149],{"class":37,"line":143},11,[35,145,146],{"class":48},"StandardOutput",[35,148,53],{"class":52},[35,150,151],{"class":52}," append:\u002Fhome\u002Fmalick\u002Fservice_name\u002Ferrors.log\n",[35,153,155,158,161],{"class":37,"line":154},12,[35,156,157],{"class":48},"StandardError",[35,159,160],{"class":52},"  =",[35,162,151],{"class":52},[35,164,166,169,172,175,178,181,184,187],{"class":37,"line":165},13,[35,167,168],{"class":48},"ExecStart",[35,170,171],{"class":52},"      =",[35,173,174],{"class":52}," \u002Fhome\u002Fmalick\u002Fservice_name\u002Fpocketbase",[35,176,177],{"class":52}," serve",[35,179,180],{"class":115}," --http",[35,182,183],{"class":52}," 127.0.0.1:8090",[35,185,186],{"class":115}," --origins",[35,188,189],{"class":52}," \"https:\u002F\u002Fspa.com\"\n",[35,191,193],{"class":37,"line":192},14,[35,194,63],{"emptyLinePlaceholder":62},[35,196,198],{"class":37,"line":197},15,[35,199,200],{"class":41},"[Install]\n",[35,202,204,207,209],{"class":37,"line":203},16,[35,205,206],{"class":48},"WantedBy",[35,208,53],{"class":52},[35,210,211],{"class":52}," multi-user.target\n",[15,213,214],{},"Then I proxy requests into the app through my Caddy service:",[25,216,220],{"className":217,"code":218,"language":219,"meta":30,"style":30},"language-sh shiki shiki-themes github-light","spa.com {\n    request_body {\n        max_size 10MB\n    }\n    reverse_proxy 127.0.0.1:8090\n    encode zstd gzip\n}\n","sh",[32,221,222,227,232,237,242,247,252],{"__ignoreMap":30},[35,223,224],{"class":37,"line":38},[35,225,226],{},"spa.com {\n",[35,228,229],{"class":37,"line":45},[35,230,231],{},"    request_body {\n",[35,233,234],{"class":37,"line":59},[35,235,236],{},"        max_size 10MB\n",[35,238,239],{"class":37,"line":66},[35,240,241],{},"    }\n",[35,243,244],{"class":37,"line":72},[35,245,246],{},"    reverse_proxy 127.0.0.1:8090\n",[35,248,249],{"class":37,"line":84},[35,250,251],{},"    encode zstd gzip\n",[35,253,254],{"class":37,"line":95},[35,255,256],{},"}\n",[10,258,260],{"id":259},"problem-and-solution","Problem and Solution",[15,262,263],{},"As you can see above, we can configure the PocketBase binary to specify where our SPA is accessible so it correctly handles CORS.",[15,265,266],{},"Browsers fire an OPTIONS request to the server to verify if they can proceed with the original request from the current origin. This happens every time a request is made from the browser. Sounds awful, right? Every request is essentially two.",[15,268,269,270,273],{},"Luckily. we can instruct the browser to cache the CORS settings so it doesn't keep firing OPTIONS requests unnecessarily needlessly.\nThis is possible thanks to the ",[32,271,272],{},"Access-Control-Max-Age"," header.",[15,275,276],{},"I looked around and it seems like PocketBase cannot set such header natively, at least not without extending the app with custom Go code.\nSo my solution was to handle it direclty at my web server, before it even reaches the app. A quick look at Caddy's documentation led me to this configuration:",[25,278,280],{"className":217,"code":279,"language":219,"meta":30,"style":30},"spa.com {\n    request_body {\n        max_size 10MB\n    }\n    @cors_preflight {\n        method OPTIONS\n        header Origin *\n    }\n    handle @cors_preflight {\n        header Access-Control-Max-Age \"86400\"\n    }\n    reverse_proxy 127.0.0.1:8090\n    encode zstd gzip\n}\n",[32,281,282,286,290,294,298,303,308,313,317,322,327,331,335,339],{"__ignoreMap":30},[35,283,284],{"class":37,"line":38},[35,285,226],{},[35,287,288],{"class":37,"line":45},[35,289,231],{},[35,291,292],{"class":37,"line":59},[35,293,236],{},[35,295,296],{"class":37,"line":66},[35,297,241],{},[35,299,300],{"class":37,"line":72},[35,301,302],{},"    @cors_preflight {\n",[35,304,305],{"class":37,"line":84},[35,306,307],{},"        method OPTIONS\n",[35,309,310],{"class":37,"line":95},[35,311,312],{},"        header Origin *\n",[35,314,315],{"class":37,"line":106},[35,316,241],{},[35,318,319],{"class":37,"line":119},[35,320,321],{},"    handle @cors_preflight {\n",[35,323,324],{"class":37,"line":131},[35,325,326],{},"        header Access-Control-Max-Age \"86400\"\n",[35,328,329],{"class":37,"line":143},[35,330,241],{},[35,332,333],{"class":37,"line":154},[35,334,246],{},[35,336,337],{"class":37,"line":165},[35,338,251],{},[35,340,341],{"class":37,"line":192},[35,342,256],{},[15,344,345],{},"Our browsers will now cache the CORS settings for 24 hours.",[347,348,349],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}",{"title":30,"searchDepth":45,"depth":45,"links":351},[352,353],{"id":12,"depth":45,"text":13},{"id":259,"depth":45,"text":260},"md",{},"\u002Fblog\u002Fcaching-preflight-requests-with-caddy","2024-11-15",{"title":5,"description":30},"blog\u002F20241115","Don't flood your server",[362,363,364],"caddy","infrastructure","web servers","EQapprvSiGBQYDX-ky-RQ6lDIBVKEcoRqvs0rM3kw_w",1775405925930]