Ensured JSON decodedoes not happen for endpoints returning empty body

This commit is contained in:
Alejandro Celaya 2022-11-19 09:29:29 +01:00
parent bd8ea17c84
commit dd9ee044eb
6 changed files with 53 additions and 51 deletions

View file

@ -79,7 +79,7 @@ export class ShlinkApiClient {
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain }); this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain });
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> => public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> =>
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain }) this.performRequest<void>(`/short-urls/${shortCode}`, 'DELETE', { domain })
.then(() => {}); .then(() => {});
public readonly updateShortUrl = async ( public readonly updateShortUrl = async (
@ -95,11 +95,11 @@ export class ShlinkApiClient {
.then(({ data, stats }) => ({ tags: data, stats })); .then(({ data, stats }) => ({ tags: data, stats }));
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> => public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
this.performRequest('/tags', 'DELETE', { tags }) this.performRequest<void>('/tags', 'DELETE', { tags })
.then(() => ({ tags })); .then(() => ({ tags }));
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> => public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> =>
this.performRequest('/tags', 'PUT', {}, { oldName, newName }) this.performRequest<void>('/tags', 'PUT', {}, { oldName, newName })
.then(() => ({ oldName, newName })); .then(() => ({ oldName, newName }));
public readonly health = async (): Promise<ShlinkHealth> => this.performRequest<ShlinkHealth>('/health', 'GET'); public readonly health = async (): Promise<ShlinkHealth> => this.performRequest<ShlinkHealth>('/health', 'GET');

View file

@ -5,13 +5,15 @@ export class HttpClient {
public fetchJson<T>(url: string, options?: RequestInit): Promise<T> { public fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
return this.fetch(url, options).then(async (resp) => { return this.fetch(url, options).then(async (resp) => {
const parsed = await resp.json();
if (!resp.ok) { if (!resp.ok) {
throw parsed; throw await resp.json();
} }
return parsed as T; try {
return (await resp.json()) as T;
} catch (e) {
return undefined as T;
}
}); });
} }

View file

@ -27,9 +27,10 @@ export const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
const [showColorPicker, toggleColorPicker, , hideColorPicker] = useToggle(); const [showColorPicker, toggleColorPicker, , hideColorPicker] = useToggle();
const { editing, error, edited, errorData } = tagEdit; const { editing, error, edited, errorData } = tagEdit;
const saveTag = handleEventPreventingDefault( const saveTag = handleEventPreventingDefault(
async () => editTag({ oldName: tag, newName: newTagName, color }) async () => {
.then(toggle) await editTag({ oldName: tag, newName: newTagName, color });
.catch(() => {}), toggle();
},
); );
const onClosed = pipe(hideColorPicker, () => edited && tagEdited({ oldName: tag, newName: newTagName, color })); const onClosed = pipe(hideColorPicker, () => edited && tagEdited({ oldName: tag, newName: newTagName, color }));

View file

@ -33,8 +33,10 @@ const initialState: TagEdition = {
export const tagEdited = createAction<EditTag>(`${REDUCER_PREFIX}/tagEdited`); export const tagEdited = createAction<EditTag>(`${REDUCER_PREFIX}/tagEdited`);
export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => { export const editTag = (
const editTag = createAsyncThunk( buildShlinkApiClient: ShlinkApiClientBuilder,
colorGenerator: ColorGenerator,
) => createAsyncThunk(
`${REDUCER_PREFIX}/editTag`, `${REDUCER_PREFIX}/editTag`,
async ({ oldName, newName, color }: EditTag, { getState }): Promise<EditTag> => { async ({ oldName, newName, color }: EditTag, { getState }): Promise<EditTag> => {
await buildShlinkApiClient(getState).editTag(oldName, newName); await buildShlinkApiClient(getState).editTag(oldName, newName);
@ -42,26 +44,23 @@ export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuild
return { oldName, newName, color }; return { oldName, newName, color };
}, },
); );
const { reducer } = createSlice({ export const tagEditReducerCreator = (editTagThunk: ReturnType<typeof editTag>) => createSlice({
name: REDUCER_PREFIX, name: REDUCER_PREFIX,
initialState, initialState,
reducers: {}, reducers: {},
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(editTag.pending, () => ({ editing: true, edited: false, error: false })); builder.addCase(editTagThunk.pending, () => ({ editing: true, edited: false, error: false }));
builder.addCase( builder.addCase(
editTag.rejected, editTagThunk.rejected,
(_, { error }) => ({ editing: false, edited: false, error: true, errorData: parseApiError(error) }), (_, { error }) => ({ editing: false, edited: false, error: true, errorData: parseApiError(error) }),
); );
builder.addCase(editTag.fulfilled, (_, { payload }) => ({ builder.addCase(editTagThunk.fulfilled, (_, { payload }) => ({
...pick(['oldName', 'newName'], payload), ...pick(['oldName', 'newName'], payload),
editing: false, editing: false,
edited: true, edited: true,
error: false, error: false,
})); }));
}, },
}); });
return { reducer, editTag };
};

View file

@ -111,15 +111,15 @@ export const tagsListReducerCreator = (
{ ...initialState, stats: payload.stats, tags: payload.tags, filteredTags: payload.tags } { ...initialState, stats: payload.stats, tags: payload.tags, filteredTags: payload.tags }
)); ));
builder.addCase(tagDeleted, (state, { payload: tag }) => ({ builder.addCase(tagDeleted, ({ tags, filteredTags, ...rest }, { payload: tag }) => ({
...state, ...rest,
tags: rejectTag(state.tags, tag), tags: rejectTag(tags, tag),
filteredTags: rejectTag(state.filteredTags, tag), filteredTags: rejectTag(filteredTags, tag),
})); }));
builder.addCase(tagEdited, (state, { payload }) => ({ builder.addCase(tagEdited, ({ tags, filteredTags, ...rest }, { payload }) => ({
...state, ...rest,
tags: state.tags.map(renameTag(payload.oldName, payload.newName)).sort(), tags: tags.map(renameTag(payload.oldName, payload.newName)).sort(),
filteredTags: state.filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(), filteredTags: filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(),
})); }));
builder.addCase(createNewVisits, (state, { payload }) => ({ builder.addCase(createNewVisits, (state, { payload }) => ({
...state, ...state,

View file

@ -7,7 +7,7 @@ import { EditTagModal } from '../helpers/EditTagModal';
import { TagsList } from '../TagsList'; import { TagsList } from '../TagsList';
import { filterTags, listTags, tagsListReducerCreator } from '../reducers/tagsList'; import { filterTags, listTags, tagsListReducerCreator } from '../reducers/tagsList';
import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete'; import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete';
import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit'; import { editTag, tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
import { TagsCards } from '../TagsCards'; import { TagsCards } from '../TagsCards';
import { TagsTable } from '../TagsTable'; import { TagsTable } from '../TagsTable';
@ -38,7 +38,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
)); ));
// Reducers // Reducers
bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'buildShlinkApiClient', 'ColorGenerator'); bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'editTag');
bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator'); bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator');
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient'); bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient');
@ -58,7 +58,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator'); bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator');
bottle.serviceFactory('tagDeleted', () => tagDeleted); bottle.serviceFactory('tagDeleted', () => tagDeleted);
bottle.serviceFactory('editTag', prop('editTag'), 'tagEditReducerCreator'); bottle.serviceFactory('editTag', editTag, 'buildShlinkApiClient', 'ColorGenerator');
bottle.serviceFactory('tagEdited', () => tagEdited); bottle.serviceFactory('tagEdited', () => tagEdited);
}; };