Skip to content

Add an escaping Html template to effect/http - #9061

Open
adam-r-kowalski wants to merge 4 commits into
Effect-TS:mainfrom
adam-r-kowalski:http-escaped-html-template
Open

adam-r-kowalski wants to merge 4 commits into
Effect-TS:mainfrom
adam-r-kowalski:http-escaped-html-template

Conversation

@adam-r-kowalski

@adam-r-kowalski adam-r-kowalski commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

HttpServerResponse.html as a template tag does not escape what it interpolates, so untrusted text reaches the page as markup:

const untrusted = "<img src=x onerror=alert(1)>"
HttpServerResponse.html`<p>${untrusted}</p>` // <p><img src=x onerror=alert(1)></p>

Changing the tag to escape would break code that interpolates markup strings, so this adds a new module instead.

Change

effect/http gets Html:

  • Html.make is a template tag returning Effect<Html, E, R>. Interpolated strings are escaped (& < > " '). Numbers and booleans are rendered, null and undefined render nothing, and arrays are rendered element by element.
  • Option and Effect interpolations work as in Template.make: effects are awaited and the result keeps their errors and services. Arrays of effects are not awaited; the docs show combining them with Effect.all.
  • Html values (from make, or Html.rawUnsafe for trusted markup) are inserted as they are, so components compose without double escaping. The brand is a symbol key, so objects from JSON.parse cannot pass as Html.
  • Html.escape and Html.isHtml are exported.
  • HttpServerResponse.html accepts an Html value as well as a string. The tag form is unchanged and still does not escape.

Safety

Escaping makes an interpolation safe in element text and in quoted values of ordinary attributes. It does not make it safe in URL attributes (javascript: passes through, so validate URLs separately), event handler, style or srcdoc attributes, script and style elements, comments, unquoted attribute values, or tag and attribute names. The module documents these limits.

Tests

test/http/Html.test.ts covers escaping in text and quoted attributes, rendering of primitives, arrays and Option, composition with rawUnsafe, effect services, errors and ordering, rejection of a JSON-forged Html value, and the exported helpers. test/http/HttpServerResponse.test.ts covers sending an Html value and the unchanged unescaped tag.

pnpm check, pnpm lint, pnpm jsdocs --check and the Html, HttpServerResponse and Template suites pass.

Changeset

effect patch, matching other additions to @stability unstable modules.

Closes EFF-2021

@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 28e05d2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 32 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-cloudflare Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@effect-janitor effect-janitor Bot added the 4.0 label Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
arbitrary-combinators.ts 38.84 KB 38.84 KB 0.00 KB (0.00%)
basic.ts 7.07 KB 7.07 KB 0.00 KB (0.00%)
batching.ts 10.68 KB 10.68 KB 0.00 KB (0.00%)
brand.ts 6.73 KB 6.73 KB 0.00 KB (0.00%)
cache.ts 10.92 KB 10.92 KB 0.00 KB (0.00%)
config.ts 22.32 KB 22.32 KB 0.00 KB (0.00%)
differ.ts 21.35 KB 21.35 KB 0.00 KB (0.00%)
http-client.ts 22.79 KB 22.79 KB 0.00 KB (0.00%)
http-router.ts 34.33 KB 34.33 KB 0.00 KB (0.00%)
logger.ts 11.25 KB 11.25 KB 0.00 KB (0.00%)
metric.ts 9.04 KB 9.04 KB 0.00 KB (0.00%)
optic.ts 6.96 KB 6.96 KB 0.00 KB (0.00%)
pubsub.ts 15.24 KB 15.24 KB 0.00 KB (0.00%)
queue.ts 12.16 KB 12.16 KB 0.00 KB (0.00%)
schedule.ts 11.27 KB 11.27 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 13.78 KB 13.78 KB 0.00 KB (0.00%)
schema-binary.ts 40.03 KB 40.03 KB 0.00 KB (0.00%)
schema-class.ts 21.11 KB 21.11 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 32.16 KB 32.16 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 27.33 KB 27.33 KB 0.00 KB (0.00%)
schema-string-transformation.ts 14.54 KB 14.54 KB 0.00 KB (0.00%)
schema-string.ts 12.04 KB 12.04 KB 0.00 KB (0.00%)
schema-template-literal.ts 16.11 KB 16.11 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 38.37 KB 38.37 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 25.36 KB 25.36 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 20.28 KB 20.28 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 20.49 KB 20.49 KB 0.00 KB (0.00%)
schema-toFormatter.ts 20.59 KB 20.59 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 25.06 KB 25.06 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 20.57 KB 20.57 KB 0.00 KB (0.00%)
schema.ts 20.33 KB 20.33 KB 0.00 KB (0.00%)
stm.ts 13.29 KB 13.29 KB 0.00 KB (0.00%)
stream.ts 10.00 KB 10.00 KB 0.00 KB (0.00%)

Brand Html with a symbol so JSON-parsed data cannot pass as trusted markup,
rename raw to rawUnsafe, document where escaping does not make an
interpolation safe, and keep succeeded interpolations synchronous.
@effect-janitor effect-janitor Bot added the enhancement New feature or request label Oct 12, 2026
@effect-bot

Copy link
Copy Markdown
Contributor

I have a design concern about shipping Html.make in this form. The escaping is useful for element text and quoted ordinary attributes, but the tag applies the same escaping in every interpolation context. These both type-check without making the input safe:

Html.make`<a href="${url}">link</a>` // javascript: URLs pass through
Html.make`<iframe srcdoc="${markup}"></iframe>` // entities are decoded, then parsed as HTML

Event-handler attributes have a similar problem: HTML entity escaping does not escape the JavaScript that runs after attribute decoding. The module documents these limits, but ordinary-looking template usage can still introduce XSS without an explicit unsafe call.

The resulting Html brand also means later templates insert the value without escaping, even when an earlier interpolation used the wrong context. Hardening the brand against JSON forgery fixes a separate issue; it cannot establish that the markup was constructed safely.

I would avoid shipping this template tag as the recommended way to render untrusted input. A better direction is to use an established renderer with context-specific handling and pass its output to HttpServerResponse.html. If helpers are needed in core, explicitly named, context-specific escaping functions would make a narrower contract clearer without implying that a whole template is safe. The current implementation can be correct under its documented limits while still being too easy to misuse.

This branch is waiting to be deployed

1 waiting deployment
fork — 28e05d23 Waiting Oct 12, 2026 by effect-bot via approval-gate #19199
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.0 enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants