|
| 1 | +import * as fs from 'node:fs'; |
| 2 | +import type * as vite from 'vite'; |
| 3 | +import type { AstroSettings } from '../types/astro.js'; |
| 4 | +import { notFoundTemplate } from '../template/4xx.js'; |
| 5 | +import { writeHtmlResponse } from './response.js'; |
| 6 | + |
| 7 | +// Vite internal prefixes that should always be allowed through |
| 8 | +const VITE_INTERNAL_PREFIXES = [ |
| 9 | + '/@vite/', |
| 10 | + '/@fs/', |
| 11 | + '/@id/', |
| 12 | + '/__vite', |
| 13 | + '/@react-refresh', |
| 14 | + '/node_modules/', |
| 15 | + '/.astro/', |
| 16 | +]; |
| 17 | + |
| 18 | +/** |
| 19 | + * Middleware that prevents Vite from serving files that exist outside |
| 20 | + * of srcDir and publicDir when accessed via direct URL navigation. |
| 21 | + * |
| 22 | + * This fixes the issue where files like /README.md are served |
| 23 | + * when they exist at the project root but aren't part of Astro's routing. |
| 24 | + */ |
| 25 | +export function routeGuardMiddleware(settings: AstroSettings): vite.Connect.NextHandleFunction { |
| 26 | + const { config } = settings; |
| 27 | + |
| 28 | + return function devRouteGuard(req, res, next) { |
| 29 | + const url = req.url; |
| 30 | + if (!url) { |
| 31 | + return next(); |
| 32 | + } |
| 33 | + |
| 34 | + // Only intercept requests that look like browser navigation (HTML requests) |
| 35 | + // Let all other requests through (JS modules, assets, Vite transforms, etc.) |
| 36 | + const accept = req.headers.accept || ''; |
| 37 | + if (!accept.includes('text/html')) { |
| 38 | + return next(); |
| 39 | + } |
| 40 | + |
| 41 | + let pathname: string; |
| 42 | + try { |
| 43 | + pathname = decodeURI(new URL(url, 'http://localhost').pathname); |
| 44 | + } catch { |
| 45 | + // Malformed URI, let other middleware handle it |
| 46 | + return next(); |
| 47 | + } |
| 48 | + |
| 49 | + // Always allow Vite internal paths through |
| 50 | + if (VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) { |
| 51 | + return next(); |
| 52 | + } |
| 53 | + |
| 54 | + // Always allow requests with query params (Vite transform requests like ?url, ?raw) |
| 55 | + if (url.includes('?')) { |
| 56 | + return next(); |
| 57 | + } |
| 58 | + |
| 59 | + // Check if the file exists in publicDir - allow if so |
| 60 | + const publicFilePath = new URL('.' + pathname, config.publicDir); |
| 61 | + if (fs.existsSync(publicFilePath)) { |
| 62 | + return next(); |
| 63 | + } |
| 64 | + |
| 65 | + // Check if the file exists in srcDir - allow if so (potential route) |
| 66 | + const srcFilePath = new URL('.' + pathname, config.srcDir); |
| 67 | + if (fs.existsSync(srcFilePath)) { |
| 68 | + return next(); |
| 69 | + } |
| 70 | + |
| 71 | + // Check if the file exists at project root (outside srcDir/publicDir) |
| 72 | + const rootFilePath = new URL('.' + pathname, config.root); |
| 73 | + if (fs.existsSync(rootFilePath)) { |
| 74 | + // File exists at root but not in srcDir or publicDir - block it |
| 75 | + const html = notFoundTemplate(pathname); |
| 76 | + return writeHtmlResponse(res, 404, html); |
| 77 | + } |
| 78 | + |
| 79 | + // File doesn't exist anywhere, let other middleware handle it |
| 80 | + return next(); |
| 81 | + }; |
| 82 | +} |
0 commit comments