Skip to content

[APPS-2953] Fix: warn once when the dev server refuses a host, and hint at upgrading old Vite - #597

Open
tyffical wants to merge 1 commit into
masterfrom
tiffany.trinh/apps-2953-guard-followups
Open

tyffical wants to merge 1 commit into
masterfrom
tiffany.trinh/apps-2953-guard-followups

Conversation

@tyffical

@tyffical tyffical commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Changes

6 changes across dev-server.ts, dev-server-request-guard.ts and their tests
What changed File
Warns on a refusal's first Host and reason, then logs repeats at debug. dev-server.ts
Caps warnings at MAX_WARNED_REJECTIONS (32) per dev server, with one notice. dev-server.ts
Adds reason to RequestRejection and exports getRequestHost. dev-server-request-guard.ts
Splits the host-not-allowed message into ALLOWED_HOSTS_HINT and VITE_UPGRADE_HINT. dev-server-request-guard.ts
Tests warn-once, the cap, per-instance state and credential-free logs. dev-server.test.ts
Tests each rejection's reason and the upgrade hint. dev-server-request-guard.test.ts

QA Instructions

yarn install && yarn build:all
yarn typecheck:all
# Expected: exit 0 ✅ VERIFIED
yarn lint
# Expected: exit 0 ✅ VERIFIED
yarn cli integrity
# Expected: exit 0, no changes to commit ✅ VERIFIED
yarn test:unit packages/plugins/apps/src/vite/dev-server-request-guard.test.ts packages/plugins/apps/src/vite/dev-server.test.ts
# Expected: Tests: 120 passed, 120 total ✅ VERIFIED
yarn test:unit
# Expected on macOS: only manifest-tags.integration and parameters.integration fail, because /var is a symlink to /private/var; a suite that hits a hook timeout under machine load passes when rerun alone ✅ VERIFIED
TMPDIR=/private/tmp yarn test:unit packages/plugins/apps/src/vite/manifest-tags.integration.test.ts packages/plugins/apps/src/vite/parameters.integration.test.ts
# Expected: Tests: 33 passed, 33 total ✅ VERIFIED
Real vite dev run: script, app files and output
  • Put the files below in one directory, then run:
W=<build-plugins checkout with this branch built> ./qa-guard-logging.sh
# Expected: cross-origin 403 with one warn; disallowed host 403 with both hints; 415; same-origin 200; 30 cap-host warns, the last with the cap notice ✅ VERIFIED
W=<build-plugins checkout with this branch built> QA_LOG_LEVEL=debug ./qa-guard-logging.sh
# Expected: the same, plus the 2 cross-origin repeats and cap-30 to cap-33 at debug ✅ VERIFIED
  • qa-guard-logging.sh:
#!/bin/bash
# Run: W=<build-plugins checkout with this branch built> [QA_LOG_LEVEL=debug] ./qa-guard-logging.sh
set -u
cd "$(dirname "$0")"; QA_DIR=$PWD
PORT=5297
BASE="http://127.0.0.1:${PORT}"
LOG="vite-dev-${QA_LOG_LEVEL:-warn}.log"
(cd app && exec env -u DD_API_KEY -u DD_APP_KEY -u DATADOG_API_KEY -u DATADOG_APP_KEY -u DD_OAUTH_ACCESS_TOKEN -u DATADOG_OAUTH_ACCESS_TOKEN \
    NODE_OPTIONS="-r $QA_DIR/loopback-only.cjs" W="$W" node "$W/node_modules/vite/bin/vite.js" > "../$LOG" 2>&1) &
PID=$!
for _ in $(seq 1 60); do grep -q 'Local:' "$LOG" && break; sleep 1; done
FN=$(node -e "const c=require('crypto');console.log(c.createHash('sha256').update('backend/greet').digest('hex')+'.greet')")
# Vite registers backend functions when it transforms the importing module.
curl -s -o /dev/null "${BASE}/main.js" -H "Host: localhost:${PORT}"
curl -s -o /dev/null "${BASE}/backend/greet.backend.ts" -H "Host: localhost:${PORT}"
post() { # $1 label, $2 route, $3 Host, $4 Origin, $5 Content-Type
    local out
    out=$(curl -s -w '\n%{http_code}' -X POST "${BASE}$2" -H "Host: $3" -H "Origin: $4" \
        -H "Content-Type: $5" --data "{\"functionName\":\"${FN}\",\"args\":[\"x\"]}")
    printf '%-26s %s  %s\n' "$1" "${out##*$'\n'}" "$(printf '%s' "${out%$'\n'*}" | tr '\n' ' ' | cut -c1-230)"
}
OWN="localhost:${PORT}"
echo "== Responses"
for i in 1 2 3; do post "cross-origin #$i" /__dd/executeAction "$OWN" "http://evil.example" application/json; done
post "disallowed host" /__dd/executeAction "tunnel.example:${PORT}" "http://tunnel.example:${PORT}" application/json
post "wrong content-type" /__dd/executeAction "$OWN" "http://${OWN}" text/plain
post "same origin" /__dd/debugBundle "$OWN" "http://${OWN}" application/json
for i in $(seq 0 33); do
    post "cap host #$i" /__dd/executeAction "cap-${i}.example:${PORT}" "http://cap-${i}.example:${PORT}" application/json > /dev/null
done
sleep 1
pkill -TERM -P "$PID"; kill "$PID"; wait 2>/dev/null
rm -rf app/node_modules/.vite
plain() { sed 's/\x1b\[[0-9;]*m//g' "$LOG"; }
echo "== Dev server terminal, excluding the cap hosts"
plain | grep -a -E 'Rejected|BLOCKED' | grep -v 'cap-[0-9]*\.example'
echo "== Rejection lines by level"
for level in warn debug; do
    printf '%-5s cross-origin=%s  cap-hosts=%s\n' "$level" \
        "$(plain | grep -c "\[$level|vite|apps\] Rejected.*own origin")" \
        "$(plain | grep -c "\[$level|vite|apps\] Rejected.*cap-[0-9]*\.example")"
done
echo "== Cap notice"
plain | grep -a "Further rejections" | sed -E 's/^(\[[^]]*\]).*This host \(("[^"]*")\).*(Further rejections.*)/\1 host \2 ... \3/'
  • app/index.html:
<!doctype html>
<html><body><script type="module" src="/main.js"></script></body></html>
  • app/main.js:
import { greet } from './backend/greet.backend.ts';
window.__greet = greet;
  • app/backend/greet.backend.ts:
export async function greet(name: string) {
    return `hello ${name}`;
}
  • app/vite.config.mjs:
// The built plugin is loaded from the checkout named by W.
const { datadogVitePlugin } = await import(`${process.env.W}/packages/published/vite-plugin/dist/src/index.mjs`);

export default {
    server: { host: '127.0.0.1', port: 5297, strictPort: true },
    plugins: [
        datadogVitePlugin({ logLevel: process.env.QA_LOG_LEVEL ?? 'warn', apps: { enable: true } }),
    ],
};
  • loopback-only.cjs:
// Preloaded with `node -r`: fails and logs any outbound socket that isn't loopback.
const net = require('net');
const original = net.Socket.prototype.connect;
net.Socket.prototype.connect = function (...args) {
    const options = typeof args[0] === 'object' ? args[0] : { port: args[0], host: args[1] };
    const host = options.host ?? 'localhost';
    if (!options.path && !['127.0.0.1', '::1', 'localhost'].includes(host)) {
        console.log(`[qa] BLOCKED outbound connection to ${host}:${options.port}`);
        process.nextTick(() => this.destroy(new Error(`blocked outbound connection to ${host}`)));
        return this;
    }
    return original.apply(this, args);
};
  • Output at the default log level:
== Responses
cross-origin #1            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
cross-origin #2            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
cross-origin #3            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
disallowed host            403  {"success":false,"error":"Blocked request. This host (\"tunnel.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js. On Vite 5 before 5.4.12 or Vite 6 before 6.0.9, upgrade Vite."}
wrong content-type         415  {"success":false,"error":"Requests must use Content-Type: application/json."}
same origin                200  async function greet(name) {   return `hello ${name}`; }  /** @param {import("./context.types").Context} $ */ async function main($) {     globalThis.$ = $;      // Register the $.Actions-based implementation for executeAction    
== Dev server terminal, excluding the cap hosts
[qa] BLOCKED outbound connection to browser-http-intake.logs.datadoghq.com:443
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("tunnel.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js. On Vite 5 before 5.4.12 or Vite 6 before 6.0.9, upgrade Vite.
[warn|vite|apps] Rejected POST /__dd/executeAction: Requests must use Content-Type: application/json.
== Rejection lines by level
warn  cross-origin=1  cap-hosts=30
debug cross-origin=0  cap-hosts=0
== Cap notice
[warn|vite|apps] host "cap-29.example" ... Further rejections are only logged with the Datadog plugin's `logLevel: 'debug'`.
  • Output with QA_LOG_LEVEL=debug:
== Responses
cross-origin #1            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
cross-origin #2            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
cross-origin #3            403  {"success":false,"error":"Blocked request. Only the dev server's own origin can call this endpoint."}
disallowed host            403  {"success":false,"error":"Blocked request. This host (\"tunnel.example\") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js. On Vite 5 before 5.4.12 or Vite 6 before 6.0.9, upgrade Vite."}
wrong content-type         415  {"success":false,"error":"Requests must use Content-Type: application/json."}
same origin                200  async function greet(name) {   return `hello ${name}`; }  /** @param {import("./context.types").Context} $ */ async function main($) {     globalThis.$ = $;      // Register the $.Actions-based implementation for executeAction    
== Dev server terminal, excluding the cap hosts
[qa] BLOCKED outbound connection to browser-http-intake.logs.datadoghq.com:443
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[debug|vite|apps] Rejected POST /__dd/executeAction: Blocked request. Only the dev server's own origin can call this endpoint.
[warn|vite|apps] Rejected POST /__dd/executeAction: Blocked request. This host ("tunnel.example") is not allowed. If you trust this host, add it to `server.allowedHosts` in vite.config.js. On Vite 5 before 5.4.12 or Vite 6 before 6.0.9, upgrade Vite.
[warn|vite|apps] Rejected POST /__dd/executeAction: Requests must use Content-Type: application/json.
== Rejection lines by level
warn  cross-origin=1  cap-hosts=30
debug cross-origin=2  cap-hosts=4
== Cap notice
[warn|vite|apps] host "cap-29.example" ... Further rejections are only logged with the Datadog plugin's `logLevel: 'debug'`.
  • Three refusals plus 29 cap hosts fill the 32-entry set, so cap-29 carries the notice.

Blast Radius

  • Only the apps plugin's dev-server /__dd routes change.
  • A refusal's first occurrence prints a warn line, at most 33 per dev server.
  • No feature flag. Risk: Low.

Documentation

🤖 Generated with Claude Code

…upgrading old Vite

A refused /__dd request now logs at warn the first time its Host and
reason are seen by a dev server, and at debug after that, so a developer
behind a tunnel or LAN hostname sees why the request failed. The set of
warned rejections is capped per dev server, with one notice once the cap
is reached.

The host-not-allowed message also tells users of Vite 5 before 5.4.12
or Vite 6 before 6.0.9, which lack server.allowedHosts, to upgrade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2953-guard-followups branch from 6006175 to 2a84069 Compare October 16, 2026 18:50
@tyffical
tyffical requested a balanced review from Copilot October 16, 2026 19:14
@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused logging and message changes have targeted test coverage and no identified blocking issues.

0 open findings

What changed in this PR

Improves visibility of rejected requests to the apps plugin’s Vite dev-server routes without flooding default logs.

Changes:

  • Warns once per Host/reason pair, with a per-server cap and debug logging afterward.
  • Adds stable rejection reasons and upgrade guidance for older Vite versions.
  • Tests deduplication, warning limits, instance isolation, and credential exclusion.
File Description
packages/​plugins/​apps/​src/​vite/​dev-server.ts Adds bounded, per-server refusal warnings.
packages/​plugins/​apps/​src/​vite/​dev-server.test.ts Tests refusal logging and deduplication.
packages/​plugins/​apps/​src/​vite/​dev-server-request-guard.ts Adds rejection reasons, shared Host extraction, and upgrade guidance.
packages/​plugins/​apps/​src/​vite/​dev-server-request-guard.test.ts Verifies rejection reasons and guidance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 2a840694e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T19:16:46.073744Z 2a84069 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tyffical
tyffical requested a review from Scott-Meyer October 16, 2026 19:30
@tyffical
tyffical marked this pull request as ready for review October 16, 2026 19:30
@tyffical
tyffical requested a review from a team as a code owner October 16, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants