Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stupid-panthers-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[breaking] strip `__data.json` from url
9 changes: 2 additions & 7 deletions packages/kit/src/runtime/server/cookie.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parse, serialize } from 'cookie';
import { has_data_suffix, normalize_path, strip_data_suffix } from '../../utils/url.js';
import { normalize_path } from '../../utils/url.js';

/**
* Tracks all cookies set during dev mode so we can emit warnings
Expand All @@ -22,12 +22,7 @@ export function get_cookies(request, url, dev, trailing_slash) {
const header = request.headers.get('cookie') ?? '';
const initial_cookies = parse(header, { decode });

const normalized_url = normalize_path(
// Remove suffix: 'foo/__data.json' would mean the cookie path is '/foo',
// whereas a direct hit of /foo would mean the cookie path is '/'
has_data_suffix(url.pathname) ? strip_data_suffix(url.pathname) : url.pathname,
trailing_slash
);
const normalized_url = normalize_path(url.pathname, trailing_slash);
// Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo'
const default_path = normalized_url.split('/').slice(0, -1).join('/') || '/';

Expand Down
22 changes: 13 additions & 9 deletions packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@ import { normalize_error } from '../../../utils/error.js';
import { once } from '../../../utils/functions.js';
import { load_server_data } from '../page/load_data.js';
import { clarify_devalue_error, handle_error_and_jsonify, serialize_data_node } from '../utils.js';
import { normalize_path, strip_data_suffix } from '../../../utils/url.js';
import { normalize_path } from '../../../utils/url.js';

export const INVALIDATED_HEADER = 'x-sveltekit-invalidated';
export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';

/**
* @param {import('types').RequestEvent} event
* @param {import('types').SSRRoute} route
* @param {import('types').SSROptions} options
* @param {import('types').SSRState} state
* @param {boolean[] | undefined} invalidated_data_nodes
* @param {import('types').TrailingSlash} trailing_slash
* @returns {Promise<Response>}
*/
export async function render_data(event, route, options, state, trailing_slash) {
export async function render_data(
event,
route,
options,
state,
invalidated_data_nodes,
trailing_slash
) {
if (!route.page) {
// requesting /__data.json should fail for a +server.js
return new Response(undefined, {
Expand All @@ -25,16 +33,12 @@ export async function render_data(event, route, options, state, trailing_slash)

try {
const node_ids = [...route.page.layouts, route.page.leaf];

const invalidated =
event.url.searchParams.get(INVALIDATED_HEADER)?.split('_').map(Boolean) ??
node_ids.map(() => true);
event.url.searchParams.delete(INVALIDATED_HEADER);
const invalidated = invalidated_data_nodes ?? node_ids.map(() => true);

let aborted = false;

const url = new URL(event.url);
url.pathname = normalize_path(strip_data_suffix(url.pathname), trailing_slash);
url.pathname = normalize_path(url.pathname, trailing_slash);

const new_event = { ...event, url };

Expand Down
24 changes: 20 additions & 4 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
strip_data_suffix
} from '../../utils/url.js';
import { exec } from '../../utils/routing.js';
import { redirect_json_response, render_data } from './data/index.js';
import { INVALIDATED_PARAM, redirect_json_response, render_data } from './data/index.js';
import { add_cookies_to_headers, get_cookies } from './cookie.js';
import { create_fetch } from './fetch.js';
import { Redirect } from '../control.js';
Expand All @@ -32,6 +32,7 @@ const default_preload = ({ type }) => type === 'js' || type === 'css';

/** @type {import('types').Respond} */
export async function respond(request, options, state) {
/** URL but stripped from the potential `/__data.json` suffix and its search param */
let url = new URL(request.url);

if (options.csrf.check_origin) {
Expand Down Expand Up @@ -70,7 +71,14 @@ export async function respond(request, options, state) {
}

const is_data_request = has_data_suffix(decoded);
if (is_data_request) decoded = strip_data_suffix(decoded) || '/';
/** @type {boolean[] | undefined} */
let invalidated_data_nodes;
if (is_data_request) {
decoded = strip_data_suffix(decoded) || '/';
url.pathname = strip_data_suffix(url.pathname) || '/';
invalidated_data_nodes = url.searchParams.get(INVALIDATED_PARAM)?.split('_').map(Boolean);
url.searchParams.delete(INVALIDATED_PARAM);
}

if (!state.prerendering?.fallback) {
// TODO this could theoretically break — should probably be inside a try-catch
Expand Down Expand Up @@ -133,7 +141,8 @@ export async function respond(request, options, state) {
}
}
},
url
url,
isDataRequest: is_data_request
};

// TODO remove this for 1.0
Expand Down Expand Up @@ -350,7 +359,14 @@ export async function respond(request, options, state) {
let response;

if (is_data_request) {
response = await render_data(event, route, options, state, trailing_slash ?? 'never');
response = await render_data(
event,
route,
options,
state,
invalidated_data_nodes,
trailing_slash ?? 'never'
);
} else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
response = await render_endpoint(event, await route.endpoint(), state);
} else if (route.page) {
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export async function handle_fatal_error(event, options, error) {
'text/html'
]);

if (has_data_suffix(event.url.pathname) || type === 'application/json') {
if (has_data_suffix(new URL(event.request.url).pathname) || type === 'application/json') {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json; charset=utf-8' }
Expand Down
11 changes: 11 additions & 0 deletions packages/kit/test/apps/basics/src/hooks.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ export const handle = sequence(
event.locals.answer = 42;
return resolve(event);
},
({ event, resolve }) => {
if (
event.request.url.includes('__data.json') &&
Copy link
Member

Choose a reason for hiding this comment

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

this would prevent contrived-but-allowed things like /__data.json/wheee, no? Don't we just want to check the end of the pathname?

Copy link
Member

Choose a reason for hiding this comment

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

This is just part of a test, not the code that users are going to run.

Copy link
Member

Choose a reason for hiding this comment

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

i definitely knew that and wasn't just being absent-minded

(event.url.pathname.endsWith('__data.json') || !event.isDataRequest)
) {
throw new Error(
'__data.json requests should have the suffix stripped from the URL and isDataRequest set to true'
);
}
return resolve(event);
},
({ event, resolve }) => {
if (event.url.pathname.includes('fetch-credentialed')) {
// Only get the cookie at the test where we know it's set to avoid polluting our logs with (correct) warnings
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ test.describe('Errors', () => {
);

const { status, name, message, stack, fancy } = read_errors(
'/errors/page-endpoint/get-implicit/__data.json'
'/errors/page-endpoint/get-implicit'
);
expect(status).toBe(undefined);
expect(name).toBe('FancyError');
Expand Down
7 changes: 6 additions & 1 deletion packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,9 +900,14 @@ export interface RequestEvent<
*/
setHeaders(headers: Record<string, string>): void;
/**
* The URL of the current page or endpoint
* The URL of the current page or endpoint.
*/
url: URL;
/**
* `true` if the request comes from the client asking for `+page/layout.server.js` data. The `url` property will be stripped of the internal information
* related to the data request in this case. Use this property instead if the distinction is important to you.
*/
isDataRequest: boolean;
}

/**
Expand Down