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
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
</template>

<script setup lang="ts">
import { useFetch } from '#imports';
import { useFetch, useRoute } from '#imports';

const apiPrefix = useRoute().query.apiPrefix ?? '/api';

const fetchError = async () => {
await useFetch('/api/server-error');
await useFetch(`${apiPrefix}/server-error`);
};

const fetchNitroFetch = async () => {
await useFetch('/api/nitro-fetch');
await useFetch(`${apiPrefix}/nitro-fetch`);
};

const fetchThirdPartyHttpError = async () => {
await useFetch('/api/third-party-http-error');
await useFetch(`${apiPrefix}/third-party-http-error`);
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { useRoute, useFetch } from '#imports';

const route = useRoute();
const param = route.params.param;
const apiPrefix = route.query.apiPrefix ?? '/api';

const fetchError = async () => {
await useFetch(`/api/param-error/${param}`);
await useFetch(`${apiPrefix}/param-error/${param}`);
};

const fetchData = async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useFetch, useRoute } from '#imports';

const route = useRoute();
const userId = route.params.userId as string;
const apiPrefix = route.query.apiPrefix ?? '/api';

const { data } = await useFetch(`/api/user/${userId}`, {
const { data } = await useFetch(`${apiPrefix}/user/${userId}`, {
server: false, // Don't fetch during SSR, only client-side
});
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"ioredis": "5.10.1",
"mongoose": "^9.10.2",
"mysql": "^2.18.1",
"nuxt": "^4.1.2"
"nuxt": "^4.6.0"
},
"devDependencies": {
"@playwright/test": "~1.63.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineEventHandler } from 'nuxt/server';

export default defineEventHandler(event => {
return {
message: 'Server middleware test endpoint',
path: event.url.pathname,
method: event.req.method,
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineEventHandler } from 'nuxt/server';

// `$fetch` is a Nitro global, not a `nuxt/server` export, so the portable copy uses `fetch`.
export default defineEventHandler(async () => {
const response = await fetch('https://example.com');
return response.text();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineEventHandler } from 'nuxt/server';

export default defineEventHandler(() => {
throw new Error('Nuxt 4 Param Server error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineEventHandler } from 'nuxt/server';

export default defineEventHandler(() => {
throw new Error('Nuxt 4 Server error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineEventHandler } from 'nuxt/server';

// Mimics ky's and got's `HTTPError`: it shares its `name` with h3's error class, but keeps the
// status on `response` instead of on the error itself.
class ThirdPartyHTTPError extends Error {
public readonly response = { status: 404 };

public constructor(message: string) {
super(message);
this.name = 'HTTPError';
}
}

export default defineEventHandler(() => {
throw new ThirdPartyHTTPError('Nuxt 4 third-party HTTPError');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineEventHandler, getRouterParam } from 'nuxt/server';

export default defineEventHandler(event => {
const userId = getRouterParam(event, 'userId');

return `UserId Param: ${userId}!`;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineEventHandler } from 'nuxt/server';

export default defineEventHandler(event => {
event.res.headers.set('x-nuxt-server-first-middleware', 'executed');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineEventHandler, getQuery } from 'nuxt/server';

export default defineEventHandler(event => {
if (getQuery(event).throwNuxtServerError === 'true') {
throw new Error('nuxt/server auth middleware error');
}

event.res.headers.set('x-nuxt-server-auth-middleware', 'executed');
});
Original file line number Diff line number Diff line change
@@ -1,94 +1,97 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test.describe('server-side errors', async () => {
test('captures api fetch error (fetched on click)', async ({ page }) => {
const errorPromise = waitForError('nuxt-4', async errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error';
});

await page.goto(`/fetch-server-routes`);
await page.getByText('Fetch Server API Error', { exact: true }).click();

const error = await errorPromise;

expect(error.transaction).toEqual('GET /api/server-error');

const exception0 = error.exception.values[0];
const exception1 = error.exception.values[1];

expect(exception0.type).toEqual('Error');
expect(exception0.value).toEqual('Nuxt 4 Server error');
expect(exception0.mechanism).toEqual({
handled: true,
type: 'chained',
exception_id: 1,
parent_id: 0,
source: 'cause',
});

expect(exception1.type).toEqual('Error');
expect(exception1.value).toEqual('Nuxt 4 Server error');
expect(exception1.mechanism).toEqual({
handled: false,
type: 'auto.function.nuxt.nitro',
exception_id: 0,
});
});

test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => {
const errorPromise = waitForError('nuxt-4', async errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error';
import { IMPORT_SURFACES } from './importSurfaces';

IMPORT_SURFACES.forEach(({ name, apiPrefix }) => {
test.describe(`server-side errors (${name})`, () => {
test('captures api fetch error (fetched on click)', async ({ page }) => {
const errorPromise = waitForError('nuxt-4', async errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error';
});

await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`);
await page.getByText('Fetch Server API Error', { exact: true }).click();

const error = await errorPromise;

expect(error.transaction).toEqual(`GET ${apiPrefix}/server-error`);

const exception0 = error.exception.values[0];
const exception1 = error.exception.values[1];

expect(exception0.type).toEqual('Error');
expect(exception0.value).toEqual('Nuxt 4 Server error');
expect(exception0.mechanism).toEqual({
handled: true,
type: 'chained',
exception_id: 1,
parent_id: 0,
source: 'cause',
});

expect(exception1.type).toEqual('Error');
expect(exception1.value).toEqual('Nuxt 4 Server error');
expect(exception1.mechanism).toEqual({
handled: false,
type: 'auto.function.nuxt.nitro',
exception_id: 0,
});
});

await page.goto(`/test-param/1234`);
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();

const error = await errorPromise;

expect(error.transaction).toEqual('GET /api/param-error/1234');

const exception0 = error.exception.values[0];
const exception1 = error.exception.values[1];

expect(exception0.type).toEqual('Error');
expect(exception0.value).toEqual('Nuxt 4 Param Server error');
expect(exception0.mechanism).toEqual({
handled: true,
type: 'chained',
exception_id: 1,
parent_id: 0,
source: 'cause',
test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => {
const errorPromise = waitForError('nuxt-4', async errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error';
});

await page.goto(`/test-param/1234?apiPrefix=${apiPrefix}`);
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();

const error = await errorPromise;

expect(error.transaction).toEqual(`GET ${apiPrefix}/param-error/1234`);

const exception0 = error.exception.values[0];
const exception1 = error.exception.values[1];

expect(exception0.type).toEqual('Error');
expect(exception0.value).toEqual('Nuxt 4 Param Server error');
expect(exception0.mechanism).toEqual({
handled: true,
type: 'chained',
exception_id: 1,
parent_id: 0,
source: 'cause',
});

expect(exception1.type).toEqual('Error');
expect(exception1.value).toEqual('Nuxt 4 Param Server error');
expect(exception1.mechanism).toEqual({
handled: false,
type: 'auto.function.nuxt.nitro',
exception_id: 0,
});
});

expect(exception1.type).toEqual('Error');
expect(exception1.value).toEqual('Nuxt 4 Param Server error');
expect(exception1.mechanism).toEqual({
handled: false,
type: 'auto.function.nuxt.nitro',
exception_id: 0,
// ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the
// hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike
// is covered by the unit tests.
test('captures a thrown third-party `HTTPError`', async ({ page }) => {
const errorPromise = waitForError('nuxt-4', async errorEvent => {
return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 4 third-party HTTPError');
});

await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`);
await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click();

const error = await errorPromise;

expect(error.transaction).toEqual(`GET ${apiPrefix}/third-party-http-error`);
expect(error.exception.values).toContainEqual(
expect.objectContaining({
value: 'Nuxt 4 third-party HTTPError',
mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }),
}),
);
});
});

// ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the
// hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike
// is covered by the unit tests.
test('captures a thrown third-party `HTTPError`', async ({ page }) => {
const errorPromise = waitForError('nuxt-4', async errorEvent => {
return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 4 third-party HTTPError');
});

await page.goto(`/fetch-server-routes`);
await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click();

const error = await errorPromise;

expect(error.transaction).toEqual('GET /api/third-party-http-error');
expect(error.exception.values).toContainEqual(
expect.objectContaining({
value: 'Nuxt 4 third-party HTTPError',
mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }),
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// The server test routes under `apiPrefix` are copies of each other that differ only in where they import from.
// Reference: https://github.com/nuxt/nuxt/pull/36275
export const IMPORT_SURFACES = [
{ name: '#imports', apiPrefix: '/api' },
{ name: 'nuxt/server', apiPrefix: '/api/nuxt-server' },
] as const;
Loading
Loading