mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Improve Short URL HTTP error semantics (#128866)
* return 409 status code on duplicate slug * support 404 error in by-slug resolution * remove mime type header for errors * harden error code type
This commit is contained in:
parent
141081ea2a
commit
dd81761869
10 changed files with 83 additions and 28 deletions
15
src/plugins/share/server/url_service/error.ts
Normal file
15
src/plugins/share/server/url_service/error.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type UrlServiceErrorCode = 'SLUG_EXISTS' | 'NOT_FOUND' | '';
|
||||
|
||||
export class UrlServiceError extends Error {
|
||||
constructor(message: string, public readonly code: UrlServiceErrorCode = '') {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from 'kibana/server';
|
||||
import { UrlServiceError } from '../../error';
|
||||
import { ServerUrlService } from '../../types';
|
||||
|
||||
export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => {
|
||||
|
@ -41,26 +42,35 @@ export const registerCreateRoute = (router: IRouter, url: ServerUrlService) => {
|
|||
if (!locator) {
|
||||
return res.customError({
|
||||
statusCode: 409,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: 'Locator not found.',
|
||||
});
|
||||
}
|
||||
|
||||
const shortUrl = await shortUrls.create({
|
||||
locator,
|
||||
params,
|
||||
slug,
|
||||
humanReadableSlug,
|
||||
});
|
||||
try {
|
||||
const shortUrl = await shortUrls.create({
|
||||
locator,
|
||||
params,
|
||||
slug,
|
||||
humanReadableSlug,
|
||||
});
|
||||
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: shortUrl.data,
|
||||
});
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: shortUrl.data,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof UrlServiceError) {
|
||||
if (error.code === 'SLUG_EXISTS') {
|
||||
return res.customError({
|
||||
statusCode: 409,
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from 'kibana/server';
|
||||
import { UrlServiceError } from '../../error';
|
||||
import { ServerUrlService } from '../../types';
|
||||
|
||||
export const registerResolveRoute = (router: IRouter, url: ServerUrlService) => {
|
||||
|
@ -26,15 +27,28 @@ export const registerResolveRoute = (router: IRouter, url: ServerUrlService) =>
|
|||
router.handleLegacyErrors(async (ctx, req, res) => {
|
||||
const slug = req.params.slug;
|
||||
const savedObjects = ctx.core.savedObjects.client;
|
||||
const shortUrls = url.shortUrls.get({ savedObjects });
|
||||
const shortUrl = await shortUrls.resolve(slug);
|
||||
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: shortUrl.data,
|
||||
});
|
||||
try {
|
||||
const shortUrls = url.shortUrls.get({ savedObjects });
|
||||
const shortUrl = await shortUrls.resolve(slug);
|
||||
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: shortUrl.data,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof UrlServiceError) {
|
||||
if (error.code === 'NOT_FOUND') {
|
||||
return res.customError({
|
||||
statusCode: 404,
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from './types';
|
|||
export * from './short_urls';
|
||||
export { registerUrlServiceRoutes } from './http/register_url_service_routes';
|
||||
export { registerUrlServiceSavedObjectType } from './saved_objects/register_url_service_saved_object_type';
|
||||
export * from './error';
|
||||
|
|
|
@ -12,6 +12,7 @@ import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/loc
|
|||
import { MemoryShortUrlStorage } from './storage/memory_short_url_storage';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { SavedObjectReference } from 'kibana/server';
|
||||
import { UrlServiceError } from '../error';
|
||||
|
||||
const setup = () => {
|
||||
const currentVersion = '1.2.3';
|
||||
|
@ -125,7 +126,7 @@ describe('ServerShortUrlClient', () => {
|
|||
url: '/app/test#foo/bar/baz',
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(new Error(`Slug "lala" already exists.`));
|
||||
).rejects.toThrowError(new UrlServiceError(`Slug "lala" already exists.`, 'SLUG_EXISTS'));
|
||||
});
|
||||
|
||||
test('can automatically generate human-readable slug', async () => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
ShortUrlData,
|
||||
LocatorData,
|
||||
} from '../../../common/url_service';
|
||||
import { UrlServiceError } from '../error';
|
||||
import type { ShortUrlStorage } from './types';
|
||||
import { validateSlug } from './util';
|
||||
|
||||
|
@ -74,7 +75,7 @@ export class ServerShortUrlClient implements IShortUrlClient {
|
|||
if (slug) {
|
||||
const isSlugTaken = await storage.exists(slug);
|
||||
if (isSlugTaken) {
|
||||
throw new Error(`Slug "${slug}" already exists.`);
|
||||
throw new UrlServiceError(`Slug "${slug}" already exists.`, 'SLUG_EXISTS');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { SavedObject, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server';
|
||||
import { ShortUrlRecord } from '..';
|
||||
import { UrlServiceError } from '../..';
|
||||
import { LEGACY_SHORT_URL_LOCATOR_ID } from '../../../../common/url_service/locators/legacy_short_url_locator';
|
||||
import { ShortUrlData } from '../../../../common/url_service/short_urls/types';
|
||||
import { ShortUrlStorage } from '../types';
|
||||
|
@ -161,7 +162,7 @@ export class SavedObjectShortUrlStorage implements ShortUrlStorage {
|
|||
});
|
||||
|
||||
if (result.saved_objects.length !== 1) {
|
||||
throw new Error('not found');
|
||||
throw new UrlServiceError('not found', 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const savedObject = result.saved_objects[0] as ShortUrlSavedObject;
|
||||
|
|
|
@ -131,8 +131,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
slug,
|
||||
});
|
||||
|
||||
expect(response1.status === 200).to.be(true);
|
||||
expect(response2.status >= 400).to.be(true);
|
||||
expect(response1.status).to.be(200);
|
||||
expect(response2.status).to.be(409);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,6 +23,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response2.body).to.eql(response1.body);
|
||||
});
|
||||
|
||||
it('returns 404 error when short URL does not exist', async () => {
|
||||
const response = await supertest.get('/api/short_url/NotExistingID');
|
||||
|
||||
expect(response.status).to.be(404);
|
||||
});
|
||||
|
||||
it('supports legacy short URLs', async () => {
|
||||
const id = 'abcdefghjabcdefghjabcdefghjabcdefghj';
|
||||
await supertest.post('/api/saved_objects/url/' + id).send({
|
||||
|
|
|
@ -26,6 +26,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response2.body).to.eql(response1.body);
|
||||
});
|
||||
|
||||
it('returns 404 error when short URL does not exist', async () => {
|
||||
const response = await supertest.get('/api/short_url/_slug/not-existing-slug');
|
||||
|
||||
expect(response.status).to.be(404);
|
||||
});
|
||||
|
||||
it('can resolve a short URL by its slug, when slugs are similar', async () => {
|
||||
const rnd = Math.round(Math.random() * 1e6) + 1;
|
||||
const now = Date.now();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue