@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.
@@ -1556,7 +1556,9 @@ async function coerceSegmentParams(match) {
1556
1556
  try {
1557
1557
  mod = await loadModule(segment.params);
1558
1558
  } catch (err) {
1559
- throw new ParamCoercionError(`Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`);
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
- throw new ParamCoercionError(err instanceof Error ? err.message : String(err));
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) return new Response(null, { status: result.status });
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") return new Response("Metadata route must export a default function", { status: 500 });
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));