Purrx

Input validation and hard limits

Validate every request before it costs anything, build a clean copy with defaults, and put hard limits on model output size, slide count and nesting depth.

12 min+35 XPHands-on

Once your tool is on the internet, some requests will be strange. A topic that's an entire book pasted by accident. A slide count of 5,000. A theme called toString. And now and then, a model reply that nests elements 200 levels deep. Each one can cost real money or freeze the server, unless it's stopped at the door.

Request body

topic, slideCount, theme

Model output

SlideML text

validateRequest
checkTreeLimits
escapeHtml

Pipeline

clean, bounded input

Layout + preview

safe to render

Anything from outside your code is untrusted: the request body and the model's reply alike. Both are checked at the boundary, before anything expensive or dangerous happens.

Validating requests

happens at the : the first line of our code that touches the request. validateRequest(body) returns either a clean value or a clear error:

  • topic: a string, trimmed, between 3 and 2,000 characters.
  • slideCount: a whole number from 3 to 12, defaulting to 6.
  • theme: one of the themes that exist, defaulting to light. Checked with Object.hasOwn, because "toString" in THEMES is true.
  • research: true only if it's exactly true.

Limiting model output

This lesson also updates compile.js to check the deck before doing real work: at most 60,000 characters, at most 14 slides, and per slide at most 16 levels of nesting and 150 elements (checkTreeLimits, written for you). A reply that breaks a limit becomes a problem, like any other.

Your project so far

26 files · 2 new or changed in this lesson

src/limits.js

// Everything that crosses into our system from outside gets checked: what users
// send us, and what the model sends back. Limits keep one bad request from
// costing a fortune, freezing the server, or crashing the layout engine.

import { THEMES } from "./theme.js";

export const LIMITS = {
  topicChars: 2000, // a longer brief is almost always a paste accident
  minSlides: 3,
  maxSlides: 12,
  xmlChars: 60_000, // about 15k tokens: far more than any real deck needs
  depth: 16, // how deeply elements may nest inside one slide
  elementsPerSlide: 150,
};

/**
 * Check a request body and return a clean copy with defaults filled in:
 *   { ok: true, value: { topic, slideCount, theme, research } }  or  { ok: false, error }
 */
export function validateRequest(body) {
  if (!body || typeof body !== "object") return { ok: false, error: "Send a JSON object." };

  const topic = typeof body.topic === "string" ? body.topic.trim() : "";
  if (topic.length < 3) return { ok: false, error: "Describe the deck in at least a few words." };
  if (topic.length > LIMITS.topicChars) return { ok: false, error: `Keep the description under ${LIMITS.topicChars} characters.` };

  const slideCount = body.slideCount === undefined ? 6 : Number(body.slideCount);
  if (!Number.isInteger(slideCount) || slideCount < LIMITS.minSlides || slideCount > LIMITS.maxSlides) {
    return { ok: false, error: `Choose between ${LIMITS.minSlides} and ${LIMITS.maxSlides} slides.` };
  }

  const theme = body.theme ?? "light";
  if (!Object.hasOwn(THEMES, theme)) return { ok: false, error: `Unknown theme "${theme}".` };

  return { ok: true, value: { topic, slideCount, theme, research: body.research === true } };
}

/** Throw if a slide tree is too deep or has too many elements to lay out safely. */
export function checkTreeLimits(tree) {
  let count = 0;
  let deepest = 0;

  const walk = (node, depth) => {
    if (node.tag === "#text") return;
    count += 1;
    deepest = Math.max(deepest, depth);
    if (depth > LIMITS.depth) return; // no need to go further: it's already too deep
    for (const child of node.children) walk(child, depth + 1);
  };
  walk(tree, 1);

  if (deepest > LIMITS.depth) throw new Error(`Elements are nested more than ${LIMITS.depth} levels deep.`);
  if (count > LIMITS.elementsPerSlide) throw new Error(`The slide has ${count} elements; the limit is ${LIMITS.elementsPerSlide}.`);
}

Key takeaways

  • Validate at the boundary, before any model call, and return a message a person can act on.
  • Build a new, clean object from the request; never pass the raw body deeper into the system.
  • Model output is untrusted too: cap its size, slide count and nesting depth.

Sign in to run the exercise

Reading is free. Writing code here needs an account so we have somewhere to keep your Gemini key and the +35 XP you are about to earn.