Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/nodes-base/credentials/WordpressApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export class WordpressApi implements ICredentialType {

test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials?.url}}/wp-json/wp/v2',
url: '/users',
baseURL: '={{$credentials?.url}}',
url: '/wp-json/wp/v2/users',
method: 'GET',
skipSslCertificateValidation: '={{$credentials.allowUnauthorizedCerts}}',
},
Expand Down
54 changes: 52 additions & 2 deletions packages/nodes-base/nodes/Wordpress/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,35 @@ import type {
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

/**
* Builds the WordPress API URL, supporting both pretty and non-pretty permalinks
* @param baseUrl The base WordPress site URL
* @param resource The API resource path (e.g., '/posts')
* @returns The full API URL
*/
function buildWordPressApiUrl(baseUrl: string, resource: string): string {
const url = baseUrl.toString();

// Remove trailing slash from base URL if present
const normalizedUrl = url.replace(/\/$/, '');

// Check if the URL already contains query parameters
const hasQueryParams = normalizedUrl.includes('?');

// For non-pretty permalinks, use ?rest_route= format
// This is more reliable than /wp-json/ which requires pretty permalinks to be enabled
// See: https://github.com/n8n-io/n8n/issues/18883
if (hasQueryParams) {
// If there are already query params, append with &
return `${normalizedUrl}&rest_route=/wp/v2${resource}`;
} else {
// Try pretty permalinks first (/wp-json/), but fall back to ?rest_route= if needed
// The initial request will use /wp-json/, and if it fails, the user should configure
// their WordPress to use pretty permalinks or we'll automatically detect it
return `${normalizedUrl}/wp-json/wp/v2${resource}`;
}
}

export async function wordpressApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
Expand All @@ -29,18 +58,39 @@ export async function wordpressApiRequest(
method,
qs,
body,
uri: uri || `${credentials.url}/wp-json/wp/v2${resource}`,
uri: uri || buildWordPressApiUrl(credentials.url as string, resource),
rejectUnauthorized: !credentials.allowUnauthorizedCerts,
json: true,
};
options = Object.assign({}, options, option);
if (Object.keys(options.body as IDataObject).length === 0) {
delete options.body;
}
const credentialType = 'wordpressApi';

try {
const credentialType = 'wordpressApi';
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
} catch (error) {
// If we get a 404 and haven't tried the non-pretty permalink format yet,
// retry with ?rest_route= format
const errorData = error as JsonObject;
if (
errorData.statusCode === 404 &&
!uri &&
options.uri &&
!options.uri.toString().includes('rest_route')
) {
const baseUrl = credentials.url as string;
const normalizedUrl = baseUrl.replace(/\/$/, '');
options.uri = `${normalizedUrl}/?rest_route=/wp/v2${resource}`;

try {
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
} catch (retryError) {
// If retry also fails, throw a NodeApiError with the retry error
throw new NodeApiError(this.getNode(), retryError as JsonObject);
}
}
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,70 @@ describe('Wordpress > GenericFunctions', () => {
});

describe('wordpressApiRequest', () => {
it('should make a successful request', async () => {
it('should make a successful request with pretty permalinks', async () => {
mockFunctions.helpers.requestWithAuthentication.mockResolvedValue({ data: 'testData' });
const result = await wordpressApiRequest.call(mockFunctions, 'GET', '/posts', {}, {});
expect(result).toEqual({ data: 'testData' });
expect(mockFunctions.helpers.requestWithAuthentication).toHaveBeenCalled();
expect(mockFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'wordpressApi',
expect.objectContaining({
uri: 'http://example.com/wp-json/wp/v2/posts',
}),
);
});

it('should throw NodeApiError on failure', async () => {
mockFunctions.helpers.requestWithAuthentication.mockRejectedValue({ message: 'fail' });
it('should retry with non-pretty permalinks on 404 error', async () => {
// First call fails with 404
mockFunctions.helpers.requestWithAuthentication
.mockRejectedValueOnce({ statusCode: 404, message: 'Not Found' })
.mockResolvedValueOnce({ data: 'testData' });

const result = await wordpressApiRequest.call(mockFunctions, 'GET', '/posts', {}, {});
expect(result).toEqual({ data: 'testData' });
expect(mockFunctions.helpers.requestWithAuthentication).toHaveBeenCalledTimes(2);

// Second call should use non-pretty permalink format
expect(mockFunctions.helpers.requestWithAuthentication).toHaveBeenNthCalledWith(
2,
'wordpressApi',
expect.objectContaining({
uri: 'http://example.com/?rest_route=/wp/v2/posts',
}),
);
});

it('should handle URL with trailing slash', async () => {
mockFunctions.getCredentials.mockResolvedValueOnce({
url: 'http://example.com/',
allowUnauthorizedCerts: false,
});
mockFunctions.helpers.requestWithAuthentication.mockResolvedValue({ data: 'testData' });

await wordpressApiRequest.call(mockFunctions, 'GET', '/posts', {}, {});

expect(mockFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'wordpressApi',
expect.objectContaining({
uri: 'http://example.com/wp-json/wp/v2/posts',
}),
);
});

it('should throw NodeApiError on non-404 failure', async () => {
mockFunctions.helpers.requestWithAuthentication.mockRejectedValue({
statusCode: 500,
message: 'Internal Server Error',
});
await expect(
wordpressApiRequest.call(mockFunctions, 'GET', '/posts', {}, {}),
).rejects.toThrow(NodeApiError);
});

it('should throw NodeApiError if both pretty and non-pretty permalinks fail', async () => {
mockFunctions.helpers.requestWithAuthentication
.mockRejectedValueOnce({ statusCode: 404, message: 'Not Found' })
.mockRejectedValueOnce({ statusCode: 404, message: 'Not Found' });

await expect(
wordpressApiRequest.call(mockFunctions, 'GET', '/posts', {}, {}),
).rejects.toThrow(NodeApiError);
Expand Down
Loading