[App Search] Add new encodePathParams helper (fixes unencoded document IDs) (#88648)
* Add encodePathParams helper to EnterpriseSearchRequestHandler This helper accomplishes two things: - Fixes 404s from the Enterprise Search server for user-generated IDs with special characters (e.g. ? char) - Allows us to simplify some of our createRequest calls - no longer will we have to grab request.params to create paths, this helper will do so for us * Update AS document route to use new helper - This was the primary view/API I was testing to fix this bug * Update remaining AS routes to use new helper - shorter, saves us a few lines + remove unnecessary payload: params that doesn't actually validate params Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8b1a228c29
commit
f4f6cb687c
|
@ -116,16 +116,40 @@ describe('EnterpriseSearchRequestHandler', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly encodes paths and query string parameters', async () => {
|
it('correctly encodes query string parameters', async () => {
|
||||||
const requestHandler = enterpriseSearchRequestHandler.createRequest({
|
const requestHandler = enterpriseSearchRequestHandler.createRequest({
|
||||||
path: '/api/some example',
|
path: '/api/example',
|
||||||
});
|
});
|
||||||
await makeAPICall(requestHandler, { query: { 'page[current]': 1 } });
|
await makeAPICall(requestHandler, { query: { 'page[current]': 1 } });
|
||||||
|
|
||||||
EnterpriseSearchAPI.shouldHaveBeenCalledWith(
|
EnterpriseSearchAPI.shouldHaveBeenCalledWith(
|
||||||
'http://localhost:3002/api/some%20example?page%5Bcurrent%5D=1'
|
'http://localhost:3002/api/example?page%5Bcurrent%5D=1'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('encodePathParams', () => {
|
||||||
|
it('correctly replaces :pathVariables with request.params', async () => {
|
||||||
|
const requestHandler = enterpriseSearchRequestHandler.createRequest({
|
||||||
|
path: '/api/examples/:example/some/:id',
|
||||||
|
});
|
||||||
|
await makeAPICall(requestHandler, { params: { example: 'hello', id: 'world' } });
|
||||||
|
|
||||||
|
EnterpriseSearchAPI.shouldHaveBeenCalledWith(
|
||||||
|
'http://localhost:3002/api/examples/hello/some/world'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly encodes path params as URI components', async () => {
|
||||||
|
const requestHandler = enterpriseSearchRequestHandler.createRequest({
|
||||||
|
path: '/api/examples/:example',
|
||||||
|
});
|
||||||
|
await makeAPICall(requestHandler, { params: { example: 'hello#@/$%^/&[]{}/";world' } });
|
||||||
|
|
||||||
|
EnterpriseSearchAPI.shouldHaveBeenCalledWith(
|
||||||
|
'http://localhost:3002/api/examples/hello%23%40%2F%24%25%5E%2F%26%5B%5D%7B%7D%2F%22%3Bworld'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('response passing', () => {
|
describe('response passing', () => {
|
||||||
|
|
|
@ -65,11 +65,12 @@ export class EnterpriseSearchRequestHandler {
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Set up API URL
|
// Set up API URL
|
||||||
|
const encodedPath = this.encodePathParams(path, request.params as Record<string, string>);
|
||||||
const queryParams = { ...(request.query as object), ...params };
|
const queryParams = { ...(request.query as object), ...params };
|
||||||
const queryString = !this.isEmptyObj(queryParams)
|
const queryString = !this.isEmptyObj(queryParams)
|
||||||
? `?${querystring.stringify(queryParams)}`
|
? `?${querystring.stringify(queryParams)}`
|
||||||
: '';
|
: '';
|
||||||
const url = encodeURI(this.enterpriseSearchUrl + path) + queryString;
|
const url = encodeURI(this.enterpriseSearchUrl) + encodedPath + queryString;
|
||||||
|
|
||||||
// Set up API options
|
// Set up API options
|
||||||
const { method } = request.route;
|
const { method } = request.route;
|
||||||
|
@ -126,6 +127,36 @@ export class EnterpriseSearchRequestHandler {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This path helper is similar to React Router's generatePath, but much simpler &
|
||||||
|
* does not use regexes. It enables us to pass a static '/foo/:bar/baz' string to
|
||||||
|
* createRequest({ path }) and have :bar be automatically replaced by the value of
|
||||||
|
* request.params.bar.
|
||||||
|
* It also (very importantly) wraps all URL request params with encodeURIComponent(),
|
||||||
|
* which is an extra layer of encoding required by the Enterprise Search server in
|
||||||
|
* order to correctly & safely parse user-generated IDs with special characters in
|
||||||
|
* their names - just encodeURI alone won't work.
|
||||||
|
*/
|
||||||
|
encodePathParams(path: string, params: Record<string, string>) {
|
||||||
|
const hasParams = path.includes(':');
|
||||||
|
if (!hasParams) {
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return path
|
||||||
|
.split('/')
|
||||||
|
.map((pathPart) => {
|
||||||
|
const isParam = pathPart.startsWith(':');
|
||||||
|
if (!isParam) {
|
||||||
|
return pathPart;
|
||||||
|
} else {
|
||||||
|
const pathParam = pathPart.replace(':', '');
|
||||||
|
return encodeURIComponent(params[pathParam]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to grab a usable error body from Enterprise Search - this isn't
|
* Attempt to grab a usable error body from Enterprise Search - this isn't
|
||||||
* always possible because some of our internal endpoints send back blank
|
* always possible because some of our internal endpoints send back blank
|
||||||
|
|
|
@ -27,12 +27,8 @@ describe('analytics routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request handler', () => {
|
it('creates a request handler', () => {
|
||||||
mockRouter.callRoute({
|
|
||||||
params: { engineName: 'some-engine' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/analytics/queries',
|
path: '/as/engines/:engineName/analytics/queries',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,12 +80,8 @@ describe('analytics routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request handler', () => {
|
it('creates a request handler', () => {
|
||||||
mockRouter.callRoute({
|
|
||||||
params: { engineName: 'some-engine', query: 'some-query' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/analytics/query/some-query',
|
path: '/as/engines/:engineName/analytics/query/:query',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,9 @@ export function registerAnalyticsRoutes({
|
||||||
query: schema.object(queriesSchema),
|
query: schema.object(queriesSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
const { engineName } = request.params;
|
path: '/as/engines/:engineName/analytics/queries',
|
||||||
|
})
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
|
||||||
path: `/as/engines/${engineName}/analytics/queries`,
|
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
@ -52,12 +48,8 @@ export function registerAnalyticsRoutes({
|
||||||
query: schema.object(querySchema),
|
query: schema.object(querySchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
const { engineName, query } = request.params;
|
path: '/as/engines/:engineName/analytics/query/:query',
|
||||||
|
})
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
|
||||||
path: `/as/engines/${engineName}/analytics/query/${query}`,
|
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,16 +200,8 @@ describe('credentials routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
const mockRequest = {
|
|
||||||
params: {
|
|
||||||
name: 'abc123',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockRouter.callRoute(mockRequest);
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/credentials/abc123',
|
path: '/as/credentials/:name',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -311,7 +303,6 @@ describe('credentials routes', () => {
|
||||||
mockRouter = new MockRouter({
|
mockRouter = new MockRouter({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
path: '/api/app_search/credentials/{name}',
|
path: '/api/app_search/credentials/{name}',
|
||||||
payload: 'params',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
registerCredentialsRoutes({
|
registerCredentialsRoutes({
|
||||||
|
@ -321,16 +312,8 @@ describe('credentials routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
const mockRequest = {
|
|
||||||
params: {
|
|
||||||
name: 'abc123',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockRouter.callRoute(mockRequest);
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/credentials/abc123',
|
path: '/as/credentials/:name',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -81,11 +81,9 @@ export function registerCredentialsRoutes({
|
||||||
body: tokenSchema,
|
body: tokenSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: '/as/credentials/:name',
|
||||||
path: `/as/credentials/${request.params.name}`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
router.delete(
|
router.delete(
|
||||||
{
|
{
|
||||||
|
@ -96,10 +94,8 @@ export function registerCredentialsRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: '/as/credentials/:name',
|
||||||
path: `/as/credentials/${request.params.name}`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,8 @@ describe('documents routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({
|
|
||||||
params: { engineName: 'some-engine' },
|
|
||||||
body: { documents: [{ foo: 'bar' }] },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/documents/new',
|
path: '/as/engines/:engineName/documents/new',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -79,10 +74,8 @@ describe('document routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } });
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/documents/1',
|
path: '/as/engines/:engineName/documents/:documentId',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -104,10 +97,8 @@ describe('document routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } });
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/documents/1',
|
path: '/as/engines/:engineName/documents/:documentId',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,11 +24,9 @@ export function registerDocumentsRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: `/as/engines/:engineName/documents/new`,
|
||||||
path: `/as/engines/${request.params.engineName}/documents/new`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,11 +44,9 @@ export function registerDocumentRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: `/as/engines/:engineName/documents/:documentId`,
|
||||||
path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
router.delete(
|
router.delete(
|
||||||
{
|
{
|
||||||
|
@ -62,10 +58,8 @@ export function registerDocumentRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: `/as/engines/:engineName/documents/:documentId`,
|
||||||
path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ describe('engine routes', () => {
|
||||||
|
|
||||||
describe('hasValidData', () => {
|
describe('hasValidData', () => {
|
||||||
it('should correctly validate that the response has data', () => {
|
it('should correctly validate that the response has data', () => {
|
||||||
|
mockRequestHandler.createRequest.mockClear();
|
||||||
const response = {
|
const response = {
|
||||||
meta: {
|
meta: {
|
||||||
page: {
|
page: {
|
||||||
|
@ -73,6 +74,7 @@ describe('engine routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly validate that a response does not have data', () => {
|
it('should correctly validate that a response does not have data', () => {
|
||||||
|
mockRequestHandler.createRequest.mockClear();
|
||||||
const response = {};
|
const response = {};
|
||||||
|
|
||||||
mockRouter.callRoute(mockRequest);
|
mockRouter.callRoute(mockRequest);
|
||||||
|
@ -125,10 +127,8 @@ describe('engine routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({ params: { name: 'some-engine' } });
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/details',
|
path: '/as/engines/:name/details',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -150,10 +150,8 @@ describe('engine routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({ params: { name: 'some-engine' } });
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/engines/some-engine/overview_metrics',
|
path: '/as/engines/:name/overview_metrics',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,11 +54,9 @@ export function registerEnginesRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: `/as/engines/:name/details`,
|
||||||
path: `/as/engines/${request.params.name}/details`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
|
@ -69,10 +67,8 @@ export function registerEnginesRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: `/as/engines/:name/overview_metrics`,
|
||||||
path: `/as/engines/${request.params.name}/overview_metrics`,
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,6 @@ describe('log settings routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({});
|
|
||||||
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/log_settings',
|
path: '/as/log_settings',
|
||||||
});
|
});
|
||||||
|
@ -52,7 +50,6 @@ describe('log settings routes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates a request to enterprise search', () => {
|
it('creates a request to enterprise search', () => {
|
||||||
mockRouter.callRoute({});
|
|
||||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||||
path: '/as/log_settings',
|
path: '/as/log_settings',
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,10 +40,8 @@ export function registerSettingsRoutes({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
enterpriseSearchRequestHandler.createRequest({
|
||||||
return enterpriseSearchRequestHandler.createRequest({
|
path: '/as/log_settings',
|
||||||
path: '/as/log_settings',
|
})
|
||||||
})(context, request, response);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue