@timber-js/app 0.2.0-alpha.107 → 0.2.0-alpha.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/internal.js +22 -4
- package/dist/server/internal.js.map +1 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/server/param-coercion.ts +23 -4
- package/src/server/pipeline-outcome.ts +9 -0
- package/src/server/pipeline-phases.ts +26 -0
package/dist/server/internal.js
CHANGED
|
@@ -1556,7 +1556,9 @@ async function coerceSegmentParams(match) {
|
|
|
1556
1556
|
try {
|
|
1557
1557
|
mod = await loadModule(segment.params);
|
|
1558
1558
|
} catch (err) {
|
|
1559
|
-
|
|
1559
|
+
const message = `Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`;
|
|
1560
|
+
if (isDebug()) console.warn(`[timber] Param coercion error: ${message}\n Segment: ${segment.segmentName}\n Params file: ${segment.params}`);
|
|
1561
|
+
throw new ParamCoercionError(message);
|
|
1560
1562
|
}
|
|
1561
1563
|
const segmentParamsDef = mod.segmentParams;
|
|
1562
1564
|
if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
|
|
@@ -1564,7 +1566,12 @@ async function coerceSegmentParams(match) {
|
|
|
1564
1566
|
const coerced = segmentParamsDef.parse(match.segmentParams);
|
|
1565
1567
|
for (const key of Object.keys(coerced)) if (key !== "__proto__") mergeTarget[key] = sanitizeParamValue(coerced[key]);
|
|
1566
1568
|
} catch (err) {
|
|
1567
|
-
|
|
1569
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1570
|
+
if (isDebug()) {
|
|
1571
|
+
const rawKeys = Object.keys(match.segmentParams).join(", ");
|
|
1572
|
+
console.warn(`[timber] Param codec rejected values for segment "${segment.segmentName}"\n Error: ${message}\n Available raw params: { ${rawKeys} }\n Params file: ${segment.params}\n Hint: if some params are only present at deeper route depths,\n make them optional in defineSegmentParams().`);
|
|
1573
|
+
}
|
|
1574
|
+
throw new ParamCoercionError(message);
|
|
1568
1575
|
}
|
|
1569
1576
|
}
|
|
1570
1577
|
}
|
|
@@ -1626,6 +1633,7 @@ async function outcomeToResponse(config, outcome, ctx) {
|
|
|
1626
1633
|
await fireOnRequestError(denyRenderError, ctx.req, "render");
|
|
1627
1634
|
if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
|
|
1628
1635
|
}
|
|
1636
|
+
if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a not-found.tsx or error.tsx to render a custom deny page.`);
|
|
1629
1637
|
return new Response(null, {
|
|
1630
1638
|
status: outcome.signal.status,
|
|
1631
1639
|
headers
|
|
@@ -1816,14 +1824,20 @@ async function handleRequest(config, req, method, path) {
|
|
|
1816
1824
|
const stripTrailingSlash = config.stripTrailingSlash ?? true;
|
|
1817
1825
|
const url = new URL(req.url);
|
|
1818
1826
|
const result = canonicalize(url.pathname, stripTrailingSlash);
|
|
1819
|
-
if (!result.ok)
|
|
1827
|
+
if (!result.ok) {
|
|
1828
|
+
if (isDebug()) console.warn(`[timber] URL canonicalization rejected ${method} ${url.pathname} — responding with ${result.status}\n This usually means the URL contains encoded separators (%2f, %5c),\n null bytes (%00), path traversal (..), or malformed percent-encoding.`);
|
|
1829
|
+
return new Response(null, { status: result.status });
|
|
1830
|
+
}
|
|
1820
1831
|
const canonicalPathname = result.pathname;
|
|
1821
1832
|
if (config.matchMetadataRoute) {
|
|
1822
1833
|
const metaMatch = config.matchMetadataRoute(canonicalPathname);
|
|
1823
1834
|
if (metaMatch) try {
|
|
1824
1835
|
if (metaMatch.isStatic) return await serveStaticMetadataFile(metaMatch);
|
|
1825
1836
|
const mod = await loadModule(metaMatch.file);
|
|
1826
|
-
if (typeof mod.default !== "function")
|
|
1837
|
+
if (typeof mod.default !== "function") {
|
|
1838
|
+
if (isDebug()) console.warn(`[timber] Metadata route ${metaMatch.file} does not export a default function — responding with 500\n Metadata routes must export a default function that returns the metadata content.`);
|
|
1839
|
+
return new Response("Metadata route must export a default function", { status: 500 });
|
|
1840
|
+
}
|
|
1827
1841
|
const handlerResult = await mod.default();
|
|
1828
1842
|
if (handlerResult instanceof Response) return cloneWithMutableHeaders(handlerResult);
|
|
1829
1843
|
const contentType = metaMatch.contentType;
|
|
@@ -1901,6 +1915,10 @@ async function handleRequest(config, req, method, path) {
|
|
|
1901
1915
|
await coerceSegmentParams(match);
|
|
1902
1916
|
} catch (error) {
|
|
1903
1917
|
if (error instanceof ParamCoercionError) {
|
|
1918
|
+
if (isDebug()) {
|
|
1919
|
+
const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
|
|
1920
|
+
console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a params.ts codec rejected the URL params.\n Check that all fields in defineSegmentParams() are optional for params\n that don't appear at every route depth (e.g. year, month, day).`);
|
|
1921
|
+
}
|
|
1904
1922
|
const leafSegment = match.segments[match.segments.length - 1];
|
|
1905
1923
|
if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
|
|
1906
1924
|
if (config.renderNoMatch) return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
|