diff --git a/.changeset/spicy-guests-protect.md b/.changeset/spicy-guests-protect.md new file mode 100644 index 000000000000..5f77d5ee0100 --- /dev/null +++ b/.changeset/spicy-guests-protect.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Trailing slash support for actions diff --git a/packages/astro/src/actions/plugins.ts b/packages/astro/src/actions/plugins.ts index 77cbddca15af..c97e3ea83573 100644 --- a/packages/astro/src/actions/plugins.ts +++ b/packages/astro/src/actions/plugins.ts @@ -1,6 +1,7 @@ import type fsMod from 'node:fs'; import type { Plugin as VitePlugin } from 'vite'; import type { AstroSettings } from '../types/astro.js'; +import { shouldAppendForwardSlash } from '../core/build/util.js'; import { NOOP_ACTIONS, RESOLVED_VIRTUAL_INTERNAL_MODULE_ID, @@ -84,6 +85,12 @@ export function vitePluginActions({ code += `\nexport * from 'astro/actions/runtime/virtual/server.js';`; } else { code += `\nexport * from 'astro/actions/runtime/virtual/client.js';`; + code = code.replace( + "'/** @TRAILING_SLASH@ **/'", + JSON.stringify( + shouldAppendForwardSlash(settings.config.trailingSlash, settings.config.build.format), + ), + ); } return code; }, diff --git a/packages/astro/src/actions/runtime/virtual/server.ts b/packages/astro/src/actions/runtime/virtual/server.ts index 47decf18e5d4..8005c4d56a06 100644 --- a/packages/astro/src/actions/runtime/virtual/server.ts +++ b/packages/astro/src/actions/runtime/virtual/server.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; import { ActionCalledFromServerError } from '../../../core/errors/errors-data.js'; import { AstroError } from '../../../core/errors/errors.js'; +import type { Pipeline } from '../../../core/base-pipeline.js'; +import { apiContextRoutesSymbol } from '../../../core/render-context.js'; +import { shouldAppendForwardSlash } from '../../../core/build/util.js'; +import { removeTrailingForwardSlash } from '../../../core/path.js'; import type { APIContext } from '../../../types/public/index.js'; import { ACTION_RPC_ROUTE_PATTERN } from '../../consts.js'; import { @@ -279,7 +283,15 @@ export function getActionContext(context: APIContext): ActionMiddlewareContext { calledFrom: callerInfo.from, name: callerInfo.name, handler: async () => { - const baseAction = await getAction(callerInfo.name); + const pipeline: Pipeline = Reflect.get(context, apiContextRoutesSymbol); + const callerInfoName = shouldAppendForwardSlash( + pipeline.manifest.trailingSlash, + pipeline.manifest.buildFormat, + ) + ? removeTrailingForwardSlash(callerInfo.name) + : callerInfo.name; + + const baseAction = await getAction(callerInfoName); let input; try { input = await parseRequestBody(context.request); diff --git a/packages/astro/src/actions/runtime/virtual/shared.ts b/packages/astro/src/actions/runtime/virtual/shared.ts index 4067ad321dfd..36fb53c379bb 100644 --- a/packages/astro/src/actions/runtime/virtual/shared.ts +++ b/packages/astro/src/actions/runtime/virtual/shared.ts @@ -2,6 +2,7 @@ import { parse as devalueParse, stringify as devalueStringify } from 'devalue'; import type { z } from 'zod'; import { REDIRECT_STATUS_CODES } from '../../../core/constants.js'; import { ActionsReturnedInvalidDataError } from '../../../core/errors/errors-data.js'; +import { appendForwardSlash as _appendForwardSlash } from '../../../core/path.js'; import { AstroError } from '../../../core/errors/errors.js'; import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from '../../consts.js'; import type { @@ -13,6 +14,8 @@ import type { export type ActionAPIContext = _ActionAPIContext; export const ACTION_QUERY_PARAMS = _ACTION_QUERY_PARAMS; +export const appendForwardSlash = _appendForwardSlash; + export const ACTION_ERROR_CODES = [ 'BAD_REQUEST', 'UNAUTHORIZED', diff --git a/packages/astro/templates/actions.mjs b/packages/astro/templates/actions.mjs index 82a287448a22..e7343448b487 100644 --- a/packages/astro/templates/actions.mjs +++ b/packages/astro/templates/actions.mjs @@ -1,4 +1,9 @@ -import { ActionError, deserializeActionResult, getActionQueryString } from 'astro:actions'; +import { + ActionError, + deserializeActionResult, + getActionQueryString, + appendForwardSlash, +} from 'astro:actions'; const ENCODED_DOT = '%2E'; @@ -83,7 +88,15 @@ async function handleAction(param, path, context) { headers.set('Content-Length', '0'); } } - const rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}`, { + + const shouldAppendTrailingSlash = '/** @TRAILING_SLASH@ **/'; + let actionPath = import.meta.env.BASE_URL.replace(/\/$/, '') + '/_actions/' + path; + + if (shouldAppendTrailingSlash) { + actionPath = appendForwardSlash(actionPath); + } + + const rawResult = await fetch(actionPath, { method: 'POST', body, headers, diff --git a/packages/astro/test/actions.test.js b/packages/astro/test/actions.test.js index d8da4e72e308..98e642102ec4 100644 --- a/packages/astro/test/actions.test.js +++ b/packages/astro/test/actions.test.js @@ -564,6 +564,30 @@ it('Base path should be used', async () => { await devServer.stop(); }); +it('Should support trailing slash', async () => { + const fixture = await loadFixture({ + root: './fixtures/actions/', + adapter: testAdapter(), + trailingSlash: "always" + }); + const devServer = await fixture.startDevServer(); + const formData = new FormData(); + formData.append('channel', 'bholmesdev'); + formData.append('comment', 'Hello, World!'); + const res = await fixture.fetch('/_actions/comment/', { + method: 'POST', + body: formData, + }); + + assert.equal(res.ok, true); + assert.equal(res.headers.get('Content-Type'), 'application/json+devalue'); + + const data = devalue.parse(await res.text()); + assert.equal(data.channel, 'bholmesdev'); + assert.equal(data.comment, 'Hello, World!'); + await devServer.stop(); +}); + /** * Follow an expected redirect response. *