diff --git a/app/features/art/actions/art.new.server.ts b/app/features/art/actions/art.new.server.ts index 166767e08..bd90d2a9a 100644 --- a/app/features/art/actions/art.new.server.ts +++ b/app/features/art/actions/art.new.server.ts @@ -20,11 +20,11 @@ import { userArtPage } from "~/utils/urls"; import { NEW_ART_EXISTING_SEARCH_PARAM_KEY } from "../art-constants"; import { editArtSchema, newArtSchema } from "../art-schemas.server"; -export const action: ActionFunction = async ({ request }) => { +export const action: ActionFunction = async ({ request, url }) => { const user = requireUser(); requireRole("ARTIST"); - const searchParams = new URL(request.url).searchParams; + const searchParams = url.searchParams; const artIdRaw = searchParams.get(NEW_ART_EXISTING_SEARCH_PARAM_KEY); // updating logic diff --git a/app/features/art/loaders/art.new.server.ts b/app/features/art/loaders/art.new.server.ts index e5714cc45..7476d9188 100644 --- a/app/features/art/loaders/art.new.server.ts +++ b/app/features/art/loaders/art.new.server.ts @@ -3,12 +3,10 @@ import { requireUser } from "~/features/auth/core/user.server"; import * as ArtRepository from "../ArtRepository.server"; import { NEW_ART_EXISTING_SEARCH_PARAM_KEY } from "../art-constants"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = requireUser(); - const artIdRaw = new URL(request.url).searchParams.get( - NEW_ART_EXISTING_SEARCH_PARAM_KEY, - ); + const artIdRaw = url.searchParams.get(NEW_ART_EXISTING_SEARCH_PARAM_KEY); if (!artIdRaw) return { art: null, tags: await ArtRepository.findAllTags() }; const artId = Number(artIdRaw); diff --git a/app/features/art/loaders/art.server.ts b/app/features/art/loaders/art.server.ts index d436b9ed2..300d4687e 100644 --- a/app/features/art/loaders/art.server.ts +++ b/app/features/art/loaders/art.server.ts @@ -4,7 +4,7 @@ import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; import * as ArtRepository from "../ArtRepository.server"; import { FILTERED_TAG_KEY_SEARCH_PARAM_KEY } from "../art-constants"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const cachedArts = await cachified({ key: "arts", cache, @@ -18,7 +18,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }, }); - const filteredTagName = new URL(request.url).searchParams.get( + const filteredTagName = url.searchParams.get( FILTERED_TAG_KEY_SEARCH_PARAM_KEY, ); diff --git a/app/features/auth/core/routes.server.ts b/app/features/auth/core/routes.server.ts index 98620e552..4fec817a0 100644 --- a/app/features/auth/core/routes.server.ts +++ b/app/features/auth/core/routes.server.ts @@ -23,8 +23,7 @@ import type { AuthErrorCode } from "./errors"; import { authSessionStorage } from "./session.server"; import { getUser } from "./user.server"; -export const callbackLoader: LoaderFunction = async ({ request }) => { - const url = new URL(request.url); +export const callbackLoader: LoaderFunction = async ({ request, url }) => { if (url.searchParams.get("error") === "access_denied") { // The user denied the authentication request // https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/ @@ -77,7 +76,7 @@ export const logInAction: ActionFunction = async ({ request }) => { return await authenticator.authenticate("discord", request); }; -export const impersonateAction: ActionFunction = async ({ request }) => { +export const impersonateAction: ActionFunction = async ({ request, url }) => { if (!DANGEROUS_CAN_ACCESS_DEV_CONTROLS) { const user = requireUser(); if (!user.roles.includes("ADMIN") && !user.roles.includes("DEV")) { @@ -85,7 +84,6 @@ export const impersonateAction: ActionFunction = async ({ request }) => { } if (user.roles.includes("DEV") && !user.roles.includes("ADMIN")) { - const url = new URL(request.url); const targetId = Number(url.searchParams.get("id")); if (isAdmin({ id: targetId }) || isStaff({ id: targetId })) { throw new Response("Forbidden", { status: 403 }); @@ -99,7 +97,6 @@ export const impersonateAction: ActionFunction = async ({ request }) => { const realUserId = session.get(SESSION_KEY); - const url = new URL(request.url); const rawId = url.searchParams.get("id"); const userId = Number(url.searchParams.get("id")); diff --git a/app/features/auth/core/user-context.server.ts b/app/features/auth/core/user-context.server.ts index d436761f2..a627b0c44 100644 --- a/app/features/auth/core/user-context.server.ts +++ b/app/features/auth/core/user-context.server.ts @@ -26,6 +26,7 @@ export function getUserContext(): UserContext { export async function getUserFromRequest( request: Request, + url: URL, ): Promise { const session = await authSessionStorage.getSession( request.headers.get("Cookie"), @@ -37,7 +38,6 @@ export async function getUserFromRequest( if (!userId) return undefined; if (userIsBanned(userId)) { - const url = new URL(request.url); const isExemptPath = url.pathname === SUSPENDED_PAGE || // needed for ban E2E tests diff --git a/app/features/auth/core/user-middleware.server.ts b/app/features/auth/core/user-middleware.server.ts index 6882d8c8a..62c0e2cd6 100644 --- a/app/features/auth/core/user-middleware.server.ts +++ b/app/features/auth/core/user-middleware.server.ts @@ -5,6 +5,7 @@ import { type MiddlewareArgs = { request: Request; + url: URL; context: unknown; }; @@ -13,8 +14,8 @@ type MiddlewareFn = ( next: () => Promise, ) => Promise; -export const userMiddleware: MiddlewareFn = async ({ request }, next) => { - const user = await getUserFromRequest(request); +export const userMiddleware: MiddlewareFn = async ({ request, url }, next) => { + const user = await getUserFromRequest(request, url); return userAsyncLocalStorage.run({ user }, () => next()); }; diff --git a/app/features/builds/loaders/builds.$slug.server.ts b/app/features/builds/loaders/builds.$slug.server.ts index 655a41ab9..db2c680bf 100644 --- a/app/features/builds/loaders/builds.$slug.server.ts +++ b/app/features/builds/loaders/builds.$slug.server.ts @@ -15,7 +15,7 @@ import { } from "../builds-schemas"; import { filterBuilds } from "../core/filter.server"; -export const loader = async ({ request, params }: LoaderFunctionArgs) => { +export const loader = async ({ request, params, url }: LoaderFunctionArgs) => { const user = getUser(); const t = await i18next.getFixedT(request, ["weapons", "common"], { lng: "en", @@ -26,7 +26,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { throw new Response(null, { status: 404 }); } - const url = new URL(request.url); const limit = buildsLimitSearchParam.parse(url.searchParams.get("limit")); const weaponName = t(`weapons:MAIN_${weaponId}`); diff --git a/app/features/calendar/loaders/calendar.new.server.ts b/app/features/calendar/loaders/calendar.new.server.ts index 20e4baf50..c7be12c54 100644 --- a/app/features/calendar/loaders/calendar.new.server.ts +++ b/app/features/calendar/loaders/calendar.new.server.ts @@ -11,12 +11,10 @@ import { requireRole } from "~/modules/permissions/guards.server"; import { tournamentBracketsPage } from "~/utils/urls"; import { canEditCalendarEvent } from "../calendar-utils"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = requireUser(); requireRole("CALENDAR_EVENT_ADDER"); - const url = new URL(request.url); - const eventWithTournament = async (key: string) => { const eventId = Number(url.searchParams.get(key)); const event = Number.isNaN(eventId) diff --git a/app/features/chat/routes/api.chat-users.ts b/app/features/chat/routes/api.chat-users.ts index 68eb06829..3229ba78c 100644 --- a/app/features/chat/routes/api.chat-users.ts +++ b/app/features/chat/routes/api.chat-users.ts @@ -2,10 +2,9 @@ import type { LoaderFunctionArgs } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { requireUser(); - const url = new URL(request.url); const idsParam = url.searchParams.get("ids"); if (!idsParam) { diff --git a/app/features/leaderboards/loaders/leaderboards.server.ts b/app/features/leaderboards/loaders/leaderboards.server.ts index c40aed663..58c99ab07 100644 --- a/app/features/leaderboards/loaders/leaderboards.server.ts +++ b/app/features/leaderboards/loaders/leaderboards.server.ts @@ -22,14 +22,10 @@ import { WEAPON_LEADERBOARD_MAX_SIZE, } from "../leaderboards-constants"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = getUser(); - const unvalidatedType = new URL(request.url).searchParams.get( - TYPE_SEARCH_PARAM_KEY, - ); - const unvalidatedSeason = new URL(request.url).searchParams.get( - SEASON_SEARCH_PARAM_KEY, - ); + const unvalidatedType = url.searchParams.get(TYPE_SEARCH_PARAM_KEY); + const unvalidatedSeason = url.searchParams.get(SEASON_SEARCH_PARAM_KEY); const type = LEADERBOARD_TYPES.find((type) => type === unvalidatedType) ?? diff --git a/app/features/sendouq/actions/q.server.ts b/app/features/sendouq/actions/q.server.ts index 58f52b851..c7eed27da 100644 --- a/app/features/sendouq/actions/q.server.ts +++ b/app/features/sendouq/actions/q.server.ts @@ -20,7 +20,7 @@ import { frontPageSchema } from "../q-schemas.server"; import { userCanJoinQueueAt } from "../q-utils"; import { SendouQError, setGroupChatMetadata } from "../q-utils.server"; -export const action: ActionFunction = async ({ request }) => { +export const action: ActionFunction = async ({ request, url }) => { const user = requireUser(); const data = await parseRequestPayload({ request, @@ -56,9 +56,7 @@ export const action: ActionFunction = async ({ request }) => { case "JOIN_TEAM": { await validateCanJoinQ(user); - const code = new URL(request.url).searchParams.get( - JOIN_CODE_SEARCH_PARAM_KEY, - ); + const code = url.searchParams.get(JOIN_CODE_SEARCH_PARAM_KEY); const groupInvitedTo = code && user ? SendouQ.findGroupByInviteCode(code) : undefined; diff --git a/app/features/sendouq/loaders/q.looking.server.ts b/app/features/sendouq/loaders/q.looking.server.ts index 28812f859..5df1fc2d5 100644 --- a/app/features/sendouq/loaders/q.looking.server.ts +++ b/app/features/sendouq/loaders/q.looking.server.ts @@ -7,11 +7,11 @@ import { SendouQ } from "../core/SendouQ.server"; import * as PrivateUserNoteRepository from "../PrivateUserNoteRepository.server"; import { sqRedirectIfNeeded } from "../q-utils.server"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = requireUser(); const isPreview = - new URL(request.url).searchParams.get("preview") === "true" && + url.searchParams.get("preview") === "true" && user.roles.includes("SUPPORTER"); const privateNotes = await PrivateUserNoteRepository.ownNotes( diff --git a/app/features/sendouq/loaders/q.server.ts b/app/features/sendouq/loaders/q.server.ts index 32dd4d96f..97f08deb1 100644 --- a/app/features/sendouq/loaders/q.server.ts +++ b/app/features/sendouq/loaders/q.server.ts @@ -6,12 +6,10 @@ import { SendouQ } from "../core/SendouQ.server"; import { JOIN_CODE_SEARCH_PARAM_KEY } from "../q-constants"; import { sqRedirectIfNeeded } from "../q-utils.server"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = getUser(); - const code = new URL(request.url).searchParams.get( - JOIN_CODE_SEARCH_PARAM_KEY, - ); + const code = url.searchParams.get(JOIN_CODE_SEARCH_PARAM_KEY); const ownGroup = user ? SendouQ.findOwnGroup(user.id) : undefined; diff --git a/app/features/team/actions/t.$customUrl.join.server.ts b/app/features/team/actions/t.$customUrl.join.server.ts index f38b45730..6caf50440 100644 --- a/app/features/team/actions/t.$customUrl.join.server.ts +++ b/app/features/team/actions/t.$customUrl.join.server.ts @@ -7,7 +7,7 @@ import * as TeamRepository from "../TeamRepository.server"; import { TEAM } from "../team-constants"; import { teamParamsSchema } from "../team-schemas.server"; -export const action: ActionFunction = async ({ request, params }) => { +export const action: ActionFunction = async ({ params, url }) => { const user = requireUser(); const { customUrl } = teamParamsSchema.parse(params); @@ -17,7 +17,7 @@ export const action: ActionFunction = async ({ request, params }) => { }), ); - const inviteCode = new URL(request.url).searchParams.get("code") ?? ""; + const inviteCode = url.searchParams.get("code") ?? ""; const realInviteCode = team.inviteCode!; errorToastIfFalsy( diff --git a/app/features/team/loaders/t.$customUrl.join.server.ts b/app/features/team/loaders/t.$customUrl.join.server.ts index 456188d96..afdc62d70 100644 --- a/app/features/team/loaders/t.$customUrl.join.server.ts +++ b/app/features/team/loaders/t.$customUrl.join.server.ts @@ -9,7 +9,7 @@ import { TEAM } from "../team-constants"; import { teamParamsSchema } from "../team-schemas.server"; import { isTeamFull, isTeamMember } from "../team-utils"; -export const loader = async ({ request, params }: LoaderFunctionArgs) => { +export const loader = async ({ params, url }: LoaderFunctionArgs) => { const user = requireUser(); const { customUrl } = teamParamsSchema.parse(params); @@ -19,7 +19,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }), ); - const inviteCode = new URL(request.url).searchParams.get("code") ?? ""; + const inviteCode = url.searchParams.get("code") ?? ""; const realInviteCode = team.inviteCode!; const teamCount = (await TeamRepository.teamsByMemberUserId(user.id)).length; diff --git a/app/features/top-search/loaders/xsearch.server.ts b/app/features/top-search/loaders/xsearch.server.ts index 98f7bd35f..3ea04724d 100644 --- a/app/features/top-search/loaders/xsearch.server.ts +++ b/app/features/top-search/loaders/xsearch.server.ts @@ -3,11 +3,10 @@ import { rankedModesShort } from "~/modules/in-game-lists/modes"; import type { RankedModeShort } from "~/modules/in-game-lists/types"; import * as XRankPlacementRepository from "../XRankPlacementRepository.server"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const availableMonthYears = await XRankPlacementRepository.monthYears(); const { month: latestMonth, year: latestYear } = availableMonthYears[0]; - const url = new URL(request.url); const mode = (() => { const mode = url.searchParams.get("mode"); if (rankedModesShort.includes(mode as any)) { diff --git a/app/features/tournament-admin/loaders/to.$id.admin.audit.server.ts b/app/features/tournament-admin/loaders/to.$id.admin.audit.server.ts index 75c3e468f..cf172425d 100644 --- a/app/features/tournament-admin/loaders/to.$id.admin.audit.server.ts +++ b/app/features/tournament-admin/loaders/to.$id.admin.audit.server.ts @@ -19,7 +19,7 @@ const auditSearchParamsSchema = z.object({ auditTeam: z.coerce.number().int().optional().catch(undefined), }); -export const loader = async ({ request, params }: LoaderFunctionArgs) => { +export const loader = async ({ request, params, url }: LoaderFunctionArgs) => { const user = requireUser(); const { id: tournamentId } = parseParams({ params, schema: idObject }); @@ -50,7 +50,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const pagesCount = Math.max(1, Math.ceil(totalCount / AUDIT_LOG_PAGE_SIZE)); - redirectIfPageOutOfBounds({ request, page, pagesCount }); + redirectIfPageOutOfBounds({ url, page, pagesCount }); return { auditLog: { diff --git a/app/features/tournament/actions/to.$id.join.server.ts b/app/features/tournament/actions/to.$id.join.server.ts index e3f936462..8f316ef21 100644 --- a/app/features/tournament/actions/to.$id.join.server.ts +++ b/app/features/tournament/actions/to.$id.join.server.ts @@ -23,13 +23,12 @@ import { requireSendouQParticipationIfNeeded, } from "../tournament-utils.server"; -export const action: ActionFunction = async ({ request, params }) => { +export const action: ActionFunction = async ({ params, url }) => { const { id: tournamentId } = parseParams({ params, schema: idObject, }); const user = requireUser(); - const url = new URL(request.url); const inviteCode = url.searchParams.get("code"); invariant(inviteCode, "code is missing"); diff --git a/app/features/tournament/loaders/to.$id.join.server.ts b/app/features/tournament/loaders/to.$id.join.server.ts index 7a092b567..c8a646c9d 100644 --- a/app/features/tournament/loaders/to.$id.join.server.ts +++ b/app/features/tournament/loaders/to.$id.join.server.ts @@ -1,8 +1,7 @@ import type { LoaderFunctionArgs } from "react-router"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; -export const loader = async ({ request }: LoaderFunctionArgs) => { - const url = new URL(request.url); +export const loader = async ({ url }: LoaderFunctionArgs) => { const inviteCode = url.searchParams.get("code"); const team = inviteCode diff --git a/app/features/user-page/loaders/u.$identifier.builds.new.server.ts b/app/features/user-page/loaders/u.$identifier.builds.new.server.ts index 30edf1315..9ac4853fa 100644 --- a/app/features/user-page/loaders/u.$identifier.builds.new.server.ts +++ b/app/features/user-page/loaders/u.$identifier.builds.new.server.ts @@ -15,9 +15,8 @@ const newBuildLoaderParamsSchema = z.object({ buildId: z.preprocess(actualNumber, id), }); -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = requireUser(); - const url = new URL(request.url); const params = newBuildLoaderParamsSchema.safeParse( Object.fromEntries(url.searchParams), diff --git a/app/features/user-page/loaders/u.$identifier.results.server.ts b/app/features/user-page/loaders/u.$identifier.results.server.ts index bfc8c0e8c..cbe41dd35 100644 --- a/app/features/user-page/loaders/u.$identifier.results.server.ts +++ b/app/features/user-page/loaders/u.$identifier.results.server.ts @@ -15,7 +15,7 @@ import { userResultsPageSearchParamsSchema } from "../user-page-schemas"; export type UserResultsLoaderData = SerializeFrom; -export const loader = async ({ params, request }: LoaderFunctionArgs) => { +export const loader = async ({ params, request, url }: LoaderFunctionArgs) => { const parsedSearchParams = parseSafeSearchParams({ request, schema: userResultsPageSearchParamsSchema, @@ -35,7 +35,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { showHighlightsOnly = false; } - const isChoosingHighlights = request.url.includes("/results/highlights"); + const isChoosingHighlights = url.pathname.includes("/results/highlights"); if (isChoosingHighlights) { showHighlightsOnly = false; } @@ -62,7 +62,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { const pagesCount = Math.ceil(totalCount / RESULTS_PER_PAGE); - redirectIfPageOutOfBounds({ request, page, pagesCount }); + redirectIfPageOutOfBounds({ url, page, pagesCount }); return { results: { diff --git a/app/features/user-page/loaders/u.$identifier.seasons.server.ts b/app/features/user-page/loaders/u.$identifier.seasons.server.ts index 8102f6eb6..33acc98ff 100644 --- a/app/features/user-page/loaders/u.$identifier.seasons.server.ts +++ b/app/features/user-page/loaders/u.$identifier.seasons.server.ts @@ -18,11 +18,11 @@ export type UserSeasonsPageLoaderData = NonNullable< SerializeFrom >; -export const loader = async ({ params, request }: LoaderFunctionArgs) => { +export const loader = async ({ params, url }: LoaderFunctionArgs) => { const loggedInUser = getUser(); const { identifier } = userParamsSchema.parse(params); const parsedSearchParams = seasonsSearchParamsSchema.safeParse( - Object.fromEntries(new URL(request.url).searchParams), + Object.fromEntries(url.searchParams), ); const user = notFoundIfFalsy( diff --git a/app/features/user-page/loaders/u.$identifier.vods.server.ts b/app/features/user-page/loaders/u.$identifier.vods.server.ts index 64a9bcbb7..9f6055bf8 100644 --- a/app/features/user-page/loaders/u.$identifier.vods.server.ts +++ b/app/features/user-page/loaders/u.$identifier.vods.server.ts @@ -9,7 +9,7 @@ import { redirectIfPageOutOfBounds, } from "~/utils/remix.server"; -export const loader = async ({ params, request }: LoaderFunctionArgs) => { +export const loader = async ({ params, request, url }: LoaderFunctionArgs) => { const userId = notFoundIfFalsy( await UserRepository.identifierToUserId(params.identifier!), ).id; @@ -30,7 +30,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { const pagesCount = Math.max(1, Math.ceil(totalCount / VODS_PAGE_BATCH_SIZE)); - redirectIfPageOutOfBounds({ request, page, pagesCount }); + redirectIfPageOutOfBounds({ url, page, pagesCount }); return { vods, diff --git a/app/features/vods/loaders/vods.new.server.ts b/app/features/vods/loaders/vods.new.server.ts index f0cc63ced..a6714ddf7 100644 --- a/app/features/vods/loaders/vods.new.server.ts +++ b/app/features/vods/loaders/vods.new.server.ts @@ -10,10 +10,9 @@ const newVodLoaderParamsSchema = z.object({ vod: z.preprocess(actualNumber, id), }); -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ url }: LoaderFunctionArgs) => { const user = requireUser(); - const url = new URL(request.url); const params = newVodLoaderParamsSchema.safeParse( Object.fromEntries(url.searchParams), ); diff --git a/app/features/vods/loaders/vods.server.ts b/app/features/vods/loaders/vods.server.ts index 6d5916231..cec833eed 100644 --- a/app/features/vods/loaders/vods.server.ts +++ b/app/features/vods/loaders/vods.server.ts @@ -7,7 +7,7 @@ import * as VodRepository from "../VodRepository.server"; import { VODS_PAGE_BATCH_SIZE } from "../vods-constants"; import { vodsSearchParamsSchema } from "../vods-schemas"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ request, url }: LoaderFunctionArgs) => { const { page, ...filters } = parseSearchParams({ request, schema: vodsSearchParamsSchema, @@ -24,7 +24,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const pagesCount = Math.max(1, Math.ceil(totalCount / VODS_PAGE_BATCH_SIZE)); - redirectIfPageOutOfBounds({ request, page, pagesCount }); + redirectIfPageOutOfBounds({ url, page, pagesCount }); return { vods, diff --git a/app/utils/Test.ts b/app/utils/Test.ts index 83ce6563e..6b250930c 100644 --- a/app/utils/Test.ts +++ b/app/utils/Test.ts @@ -87,7 +87,10 @@ export function wrappedAction({ ], }); - const userFromRequest = await getUserFromRequest(request); + const userFromRequest = await getUserFromRequest( + request, + new URL(request.url), + ); return userAsyncLocalStorage.run({ user: userFromRequest }, async () => { try { @@ -137,7 +140,10 @@ export function wrappedLoader({ ], }); - const userFromRequest = await getUserFromRequest(request); + const userFromRequest = await getUserFromRequest( + request, + new URL(request.url), + ); return userAsyncLocalStorage.run({ user: userFromRequest }, async () => { try { diff --git a/app/utils/remix.server.test.ts b/app/utils/remix.server.test.ts index 133e65371..ced032f1f 100644 --- a/app/utils/remix.server.test.ts +++ b/app/utils/remix.server.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { redirectIfPageOutOfBounds } from "./remix.server"; -const buildRequest = (url: string) => new Request(url); +const buildUrl = (url: string) => new URL(url); const captureRedirect = (fn: () => void) => { try { @@ -17,7 +17,7 @@ describe("redirectIfPageOutOfBounds()", () => { it("does not redirect when page is within bounds", () => { const response = captureRedirect(() => redirectIfPageOutOfBounds({ - request: buildRequest("https://sendou.ink/vods?page=2"), + url: buildUrl("https://sendou.ink/vods?page=2"), page: 2, pagesCount: 5, }), @@ -29,7 +29,7 @@ describe("redirectIfPageOutOfBounds()", () => { it("does not redirect when page equals pagesCount", () => { const response = captureRedirect(() => redirectIfPageOutOfBounds({ - request: buildRequest("https://sendou.ink/vods?page=5"), + url: buildUrl("https://sendou.ink/vods?page=5"), page: 5, pagesCount: 5, }), @@ -41,7 +41,7 @@ describe("redirectIfPageOutOfBounds()", () => { it("redirects to last page when page exceeds pagesCount", () => { const response = captureRedirect(() => redirectIfPageOutOfBounds({ - request: buildRequest("https://sendou.ink/vods?page=99"), + url: buildUrl("https://sendou.ink/vods?page=99"), page: 99, pagesCount: 5, }), @@ -54,7 +54,7 @@ describe("redirectIfPageOutOfBounds()", () => { it("preserves other search params when redirecting", () => { const response = captureRedirect(() => redirectIfPageOutOfBounds({ - request: buildRequest( + url: buildUrl( "https://sendou.ink/vods?type=TOURNAMENT&page=99&mode=SZ", ), page: 99, @@ -74,7 +74,7 @@ describe("redirectIfPageOutOfBounds()", () => { it("does not redirect on page 1 when pagesCount is 0 (empty results)", () => { const response = captureRedirect(() => redirectIfPageOutOfBounds({ - request: buildRequest("https://sendou.ink/vods?page=1"), + url: buildUrl("https://sendou.ink/vods?page=1"), page: 1, pagesCount: 0, }), @@ -86,7 +86,7 @@ describe("redirectIfPageOutOfBounds()", () => { it("redirects to page 1 when pagesCount is 0 and page exceeds 1", () => { const response = captureRedirect(() => redirectIfPageOutOfBounds({ - request: buildRequest("https://sendou.ink/vods?page=4"), + url: buildUrl("https://sendou.ink/vods?page=4"), page: 4, pagesCount: 0, }), diff --git a/app/utils/remix.server.ts b/app/utils/remix.server.ts index c2af564cd..0441ccfca 100644 --- a/app/utils/remix.server.ts +++ b/app/utils/remix.server.ts @@ -68,18 +68,17 @@ export function parseSearchParams({ * to a minimum of 1 so empty result sets stay on page 1. */ export function redirectIfPageOutOfBounds({ - request, + url, page, pagesCount, }: { - request: Request; + url: URL; page: number; pagesCount: number; }): void { const safePagesCount = Math.max(1, pagesCount); if (page <= safePagesCount) return; - const url = new URL(request.url); const searchParams = new URLSearchParams(url.searchParams); searchParams.set("page", String(safePagesCount)); throw redirect(`${url.pathname}?${searchParams.toString()}`); diff --git a/react-router.config.ts b/react-router.config.ts index 3dd6707b5..12c01e089 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -8,6 +8,13 @@ export default { routeDiscovery: { mode: "initial" }, future: { v8_middleware: true, + v8_splitRouteModules: true, + // Disabled: passing the raw request makes relative redirects (e.g. the + // successToast/errorToast `redirect("?__success=...")` pattern) resolve + // against the `.data` URL of single-fetch requests, breaking navigation. + v8_passThroughRequests: false, + v8_trailingSlashAwareDataRequests: true, + v8_viteEnvironmentApi: true, }, buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => { await sentryOnBuildEnd({