Fix fetch (missing request headers etc.)

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-05-23 18:16:10 +02:00
parent 8ea94e548b
commit 6daa4672d2
23 changed files with 79 additions and 40 deletions

View File

@@ -4,7 +4,7 @@ import { selectAccount } from 'soapbox/selectors';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
import api, { getLinks } from '../api';
import api, { getLinks, type PlfeResponse } from '../api';
import {
importFetchedAccount,
@@ -117,7 +117,7 @@ const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
const maybeRedirectLogin = (error: { response: Response }, history?: History) => {
const maybeRedirectLogin = (error: { response: PlfeResponse }, history?: History) => {
// The client is unauthorized - redirect to login.
if (history && error?.response?.status === 401) {
history.push('/login');

View File

@@ -26,7 +26,7 @@ import { normalizeUsername } from 'soapbox/utils/input';
import { getScopes } from 'soapbox/utils/scopes';
import { isStandalone } from 'soapbox/utils/state';
import api, { getFetch } from '../api';
import api, { type PlfeResponse, getFetch } from '../api';
import { importFetchedAccount } from './importer';
@@ -187,7 +187,7 @@ const loadCredentials = (token: string, accountUrl: string) =>
const logIn = (username: string, password: string) =>
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() =>
dispatch(createUserToken(normalizeUsername(username), password)),
).catch((error: { response: Response }) => {
).catch((error: { response: PlfeResponse }) => {
if ((error.response?.json as any)?.error === 'mfa_required') {
// If MFA is required, throw the error and handle it in the component.
throw error;

View File

@@ -1,6 +1,6 @@
import { defineMessages } from 'react-intl';
import api, { getLinks } from 'soapbox/api';
import api, { type PlfeResponse, getLinks } from 'soapbox/api';
import { normalizeAccount } from 'soapbox/normalizers';
import toast from 'soapbox/toast';
@@ -48,7 +48,7 @@ const fileExport = (content: string, fileName: string) => {
document.body.removeChild(fileToDownload);
};
const listAccounts = (getState: () => RootState) => async(apiResponse: Response & { json: any }) => {
const listAccounts = (getState: () => RootState) => async(apiResponse: PlfeResponse) => {
const followings = apiResponse.json;
let accounts = [];
let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');

View File

@@ -192,6 +192,8 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
const statuses = deduplicateStatuses(response.json);
dispatch(importFetchedStatuses(statuses.filter(status => status.accounts)));
console.log(response);
dispatch(expandTimelineSuccess(
timelineId,
statuses,

View File

@@ -11,6 +11,8 @@ import { RootState } from 'soapbox/store';
import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth';
import { buildFullPath } from 'soapbox/utils/url';
type PlfeResponse<T = any> = Response & { data: string; json: T };
/**
Parse Link headers, mostly for pagination.
@param {object} response - Fetch API response object
@@ -57,7 +59,7 @@ const getFetch = (accessToken?: string | null, baseURL: string = '') =>
// Fetch API doesn't report upload progress, use XHR
if (init?.onUploadProgress) {
return new Promise<Response & { data: string; json: T }>((resolve, reject) => {
return new Promise<PlfeResponse<T>>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('progress', init.onUploadProgress!);
@@ -72,7 +74,7 @@ const getFetch = (accessToken?: string | null, baseURL: string = '') =>
}
if (xhr.status >= 400) reject({ response: { status: xhr.status, data, json } });
resolve({ status: xhr.status, data, json } as any);
resolve({ status: xhr.status, data, json } as any as PlfeResponse<T>);
});
xhr.open(init?.method || 'GET', fullPath, true);
@@ -85,8 +87,8 @@ const getFetch = (accessToken?: string | null, baseURL: string = '') =>
return fetch(fullPath, {
...init,
headers,
}).then(async (response) => {
const data = await response.text();
}).then(async (res) => {
const data = await res.text();
let json: T = undefined!;
try {
@@ -95,11 +97,14 @@ const getFetch = (accessToken?: string | null, baseURL: string = '') =>
//
}
if (!response.ok) {
const { headers, ok, redirected, status, statusText, type, url } = response;
throw { response: { headers, ok, redirected, status, statusText, type, url, data, json } };
const { headers, ok, redirected, status, statusText, type, url } = res;
const response = { headers, ok, redirected, status, statusText, type, url, data, json };
if (!ok) {
throw { response };
}
return { ...response, data, json };
return response as any as PlfeResponse<T>;
});
};
@@ -120,7 +125,10 @@ const staticFetch = (input: URL | RequestInfo, init?: RequestInit | undefined) =
} catch (e) {
//
}
return { ...response, data, json };
const { headers, ok, redirected, status, statusText, type, url } = response;
return { headers, ok, redirected, status, statusText, type, url, data, json } as any as PlfeResponse;
});
};
@@ -141,6 +149,7 @@ const api = (getState: () => RootState, authType: string = 'user') => {
};
export {
type PlfeResponse,
getLinks,
getNextLink,
getPrevLink,

View File

@@ -1,4 +1,5 @@
import type { Entity } from '../types';
import type { PlfeResponse } from 'soapbox/api';
import type z from 'zod';
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
@@ -32,9 +33,9 @@ interface EntityCallbacks<Value, Error = unknown> {
/**
* Passed into hooks to make requests.
* Must return an Axios response.
* Must return a response.
*/
type EntityFn<T> = (value: T) => Promise<Response>
type EntityFn<T> = (value: T) => Promise<PlfeResponse>
export type {
EntitySchema,

View File

@@ -9,6 +9,7 @@ import { parseEntitiesPath } from './utils';
import type { EntityCallbacks, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
import type { Entity } from '../types';
import type { PlfeResponse } from 'soapbox/api';
interface UseCreateEntityOpts<TEntity extends Entity = Entity> {
schema?: EntitySchema<TEntity>;
@@ -26,7 +27,7 @@ const useCreateEntity = <TEntity extends Entity = Entity, Data = unknown>(
const createEntity = async (
data: Data,
callbacks: EntityCallbacks<TEntity, { response?: Response & { json: any } }> = {},
callbacks: EntityCallbacks<TEntity, { response?: PlfeResponse }> = {},
): Promise<void> => {
const result = await setPromise(entityFn(data));
const schema = opts.schema || z.custom<TEntity>();

View File

@@ -10,6 +10,7 @@ import { selectEntity } from '../selectors';
import type { EntitySchema, EntityPath, EntityFn } from './types';
import type { Entity } from '../types';
import type { PlfeResponse } from 'soapbox/api';
/** Additional options for the hook. */
interface UseEntityOpts<TEntity extends Entity> {
@@ -66,8 +67,8 @@ const useEntity = <TEntity extends Entity>(
isLoading,
isLoaded,
error,
isUnauthorized: (error as { response?: Response })?.response?.status === 401,
isForbidden: (error as { response?: Response })?.response?.status === 403,
isUnauthorized: (error as { response?: PlfeResponse })?.response?.status === 401,
isForbidden: (error as { response?: PlfeResponse })?.response?.status === 403,
};
};

View File

@@ -7,10 +7,11 @@ import { useLoading } from 'soapbox/hooks/useLoading';
import { importEntities } from '../actions';
import { findEntity } from '../selectors';
import { Entity } from '../types';
import { EntityFn } from './types';
import { type UseEntityOpts } from './useEntity';
import type { EntityFn } from './types';
import type { UseEntityOpts } from './useEntity';
import type { Entity } from '../types';
import type { PlfeResponse } from 'soapbox/api';
/** Entities will be filtered through this function until it returns true. */
type LookupFn<TEntity extends Entity> = (entity: TEntity) => boolean
@@ -56,8 +57,8 @@ const useEntityLookup = <TEntity extends Entity>(
fetchEntity,
isFetching,
isLoading,
isUnauthorized: (error as { response?: Response })?.response?.status === 401,
isForbidden: (error as { response?: Response })?.response?.status === 403,
isUnauthorized: (error as { response?: PlfeResponse })?.response?.status === 401,
isForbidden: (error as { response?: PlfeResponse })?.response?.status === 403,
};
};

View File

@@ -31,6 +31,8 @@ import { isDefaultHeader } from 'soapbox/utils/accounts';
import copy from 'soapbox/utils/copy';
import { MASTODON, parseVersion } from 'soapbox/utils/features';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
@@ -93,7 +95,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
const createAndNavigateToChat = useMutation({
mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId),
onError: (error: { response: Response }) => {
onError: (error: { response: PlfeResponse }) => {
const data = error.response?.json as any;
toast.error(data?.error);
},

View File

@@ -15,6 +15,8 @@ import ConsumersList from './consumers-list';
import LoginForm from './login-form';
import OtpAuthForm from './otp-auth-form';
import type { PlfeResponse } from 'soapbox/api';
const LoginPage = () => {
const dispatch = useAppDispatch();
@@ -49,7 +51,7 @@ const LoginPage = () => {
} else {
setShouldRedirect(true);
}
}).catch((error: { response: Response }) => {
}).catch((error: { response: PlfeResponse }) => {
const data: any = error.response?.json;
if (data?.error === 'mfa_required') {
setMfaAuthNeeded(true);

View File

@@ -15,6 +15,8 @@ import Blankslate from './blankslate';
import EmptyResultsBlankslate from './empty-results-blankslate';
import Results from './results';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
placeholder: { id: 'chat_search.placeholder', defaultMessage: 'Type a name' },
});
@@ -44,7 +46,7 @@ const ChatSearch = (props: IChatSearch) => {
const handleClickOnSearchResult = useMutation({
mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId),
onError: (error: { response: Response }) => {
onError: (error: { response: PlfeResponse }) => {
const data = error.response?.json as any;
toast.error(data?.error);
},

View File

@@ -12,6 +12,7 @@ import toast from 'soapbox/toast';
import ChatComposer from './chat-composer';
import ChatMessageList from './chat-message-list';
import type { PlfeResponse } from 'soapbox/api';
import type { Attachment } from 'soapbox/types/entities';
const fileKeyGen = (): number => Math.floor((Math.random() * 0x10000));
@@ -69,7 +70,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
onSuccess: () => {
setErrorMessage(undefined);
},
onError: (error: { response: Response & { json: any } }, _variables, context) => {
onError: (error: { response: PlfeResponse }, _variables, context) => {
const message = error.response?.json?.error;
setErrorMessage(message || intl.formatMessage(messages.failedToSend));
setContent(context.prevContent as string);

View File

@@ -11,6 +11,7 @@ import toast from 'soapbox/toast';
import ColumnForbidden from '../ui/components/column-forbidden';
import type { PlfeResponse } from 'soapbox/api';
import type { Account as AccountEntity } from 'soapbox/schemas';
type RouteParams = { groupId: string };
@@ -80,7 +81,7 @@ const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params })
const handleAuthorize = async (account: AccountEntity) =>
authorize(account.id)
.then(() => Promise.resolve())
.catch((error: { response: Response }) => {
.catch((error: { response: PlfeResponse }) => {
refetch();
let message = intl.formatMessage(messages.authorizeFail, { name: account.username });
@@ -95,7 +96,7 @@ const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params })
const handleReject = async (account: AccountEntity) =>
reject(account.id)
.then(() => Promise.resolve())
.catch((error: { response: Response }) => {
.catch((error: { response: PlfeResponse }) => {
refetch();
let message = intl.formatMessage(messages.rejectFail, { name: account.username });

View File

@@ -10,6 +10,8 @@ import toast from 'soapbox/toast';
import { isDefaultAvatar } from 'soapbox/utils/accounts';
import resizeImage from 'soapbox/utils/resize-image';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
});
@@ -48,7 +50,7 @@ const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
setDisabled(false);
setSubmitting(false);
onNext();
}).catch((error: { response: Response }) => {
}).catch((error: { response: PlfeResponse }) => {
setSubmitting(false);
setDisabled(false);
setSelectedFile(null);

View File

@@ -7,6 +7,8 @@ import { Button, FormGroup, Stack, Textarea } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import toast from 'soapbox/toast';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' },
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
@@ -30,7 +32,7 @@ const BioStep = ({ onNext }: { onNext: () => void }) => {
.then(() => {
setSubmitting(false);
onNext();
}).catch((error: { response: Response }) => {
}).catch((error: { response: PlfeResponse }) => {
setSubmitting(false);
if (error.response?.status === 422) {

View File

@@ -11,6 +11,8 @@ import toast from 'soapbox/toast';
import { isDefaultHeader } from 'soapbox/utils/accounts';
import resizeImage from 'soapbox/utils/resize-image';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
@@ -51,7 +53,7 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
setDisabled(false);
setSubmitting(false);
onNext();
}).catch((error: { response: Response }) => {
}).catch((error: { response: PlfeResponse }) => {
setSubmitting(false);
setDisabled(false);
setSelectedFile(null);

View File

@@ -7,6 +7,8 @@ import { Button, FormGroup, Input, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import toast from 'soapbox/toast';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
usernamePlaceholder: { id: 'onboarding.display_name.placeholder', defaultMessage: 'Eg. John Smith' },
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
@@ -41,7 +43,7 @@ const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
.then(() => {
setSubmitting(false);
onNext();
}).catch((error: { response: Response }) => {
}).catch((error: { response: PlfeResponse }) => {
setSubmitting(false);
if (error.response?.status === 422) {

View File

@@ -11,6 +11,8 @@ import ConfirmationStep from './steps/confirmation-step';
import DetailsStep from './steps/details-step';
import PrivacyStep from './steps/privacy-step';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
next: { id: 'manage_group.next', defaultMessage: 'Next' },
create: { id: 'manage_group.create', defaultMessage: 'Create Group' },
@@ -64,7 +66,7 @@ const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
setCurrentStep(Steps.THREE);
setGroup(group);
},
onError(error: { response?: Response }) {
onError(error: { response?: PlfeResponse }) {
const msg = z.object({ error: z.string() }).safeParse(error?.response?.json);
if (msg.success) {
toast.error(msg.data.error);

View File

@@ -13,6 +13,8 @@ import { useAppDispatch, useFeatures, useOwnAccount, useRegistrationStatus } fro
import ProfileDropdown from './profile-dropdown';
import type { PlfeResponse } from 'soapbox/api';
const messages = defineMessages({
login: { id: 'navbar.login.action', defaultMessage: 'Log in' },
username: { id: 'navbar.login.username.placeholder', defaultMessage: 'Email or username' },
@@ -50,7 +52,7 @@ const Navbar = () => {
.then(() => dispatch(fetchInstance()))
);
})
.catch((error: { response: Response }) => {
.catch((error: { response: PlfeResponse }) => {
setLoading(false);
const data: any = error.response?.json;

View File

@@ -18,6 +18,7 @@ import {
import { ME_FETCH_SKIP } from '../actions/me';
import type { AnyAction } from 'redux';
import type { PlfeResponse } from 'soapbox/api';
import type { APIEntity, Account as AccountEntity } from 'soapbox/types/entities';
const AuthAppRecord = ImmutableRecord({
@@ -282,7 +283,7 @@ const persistAuthAccount = (account: APIEntity) => {
}
};
const deleteForbiddenToken = (state: State, error: { response: Response }, token: string) => {
const deleteForbiddenToken = (state: State, error: { response: PlfeResponse }, token: string) => {
if ([401, 403].includes(error.response?.status!)) {
return deleteToken(state, token);
} else {

View File

@@ -11,11 +11,12 @@ import {
} from '../actions/me';
import type { AnyAction } from 'redux';
import type { PlfeResponse } from 'soapbox/api';
import type { Me } from 'soapbox/types/soapbox';
const initialState: Me = null;
const handleForbidden = (state: Me, error: { response: Response }) => {
const handleForbidden = (state: Me, error: { response: PlfeResponse }) => {
if (([401, 403] as any[]).includes(error.response?.status)) {
return false;
} else {

View File

@@ -5,6 +5,8 @@ import { defineMessages, MessageDescriptor } from 'react-intl';
import { Toast } from './components/ui';
import { httpErrorMessages } from './utils/errors';
import type { PlfeResponse } from './api';
type ToastText = string | MessageDescriptor
type ToastType = 'success' | 'error' | 'info'
@@ -39,7 +41,7 @@ const messages = defineMessages({
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'Something went wrong.' },
});
const showAlertForError = (networkError: { response: Response & { json: any } }) => {
const showAlertForError = (networkError: { response: PlfeResponse }) => {
if (networkError?.response) {
const { json, status, statusText } = networkError.response;