Skip to content

Commit

Permalink
Use HTTP problem details for error handling (#52)
Browse files Browse the repository at this point in the history
* Use HTTP problem details for error handling

* Prevent regression

* Lint

---------

Co-authored-by: Pete Edwards <[email protected]>
  • Loading branch information
NSeydoux and edwardsph authored Sep 13, 2024
1 parent e3d344b commit a52ff1a
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 47 deletions.
11 changes: 6 additions & 5 deletions api/apiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//
import * as SecureStore from "expo-secure-store";
import { router } from "expo-router";
import { handleErrorResponse } from "@inrupt/solid-client-errors";

export const SESSION_KEY = "session";

Expand Down Expand Up @@ -52,12 +53,12 @@ export const makeApiRequest = async <T>(
throw new Error(`Unauthorized: ${response.status}`);
}

if (response.status === 404) {
return null as T;
}

if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
throw handleErrorResponse(
response,
await response.text(),
`${endpoint} returned an error response.`
);
}

const responseType = response.headers.get("content-type");
Expand Down
75 changes: 51 additions & 24 deletions api/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
// limitations under the License.
//
import mime from "mime";
import FormData from "form-data";
import type { WalletFile } from "@/types/WalletFile";
import { handleErrorResponse } from "@inrupt/solid-client-errors";
import { makeApiRequest } from "./apiRequest";

interface FileObject {
Expand All @@ -27,38 +29,63 @@ export const fetchFiles = async (): Promise<WalletFile[]> => {
return makeApiRequest<WalletFile[]>("wallet");
};

export const postFile = async (file: FileObject): Promise<void> => {
const formData = new FormData();
export const postFile = async (fileMetadata: FileObject): Promise<void> => {
const acceptValue = fileMetadata.type ?? mime.getType(fileMetadata.name);
const acceptHeader = new Headers();
if (acceptValue !== null) {
acceptHeader.append("Accept", acceptValue);
}
// Make a HEAD request to the file to report on potential errors
// in more details than the fetch with the FormData.
const fileResponse = await fetch(fileMetadata.uri, {
headers: acceptHeader,
method: "HEAD",
});
if (!fileResponse.ok) {
throw handleErrorResponse(
fileResponse,
await fileResponse.text(),
"Failed to fetch file to upload"
);
}
// The following is declared as `any` because there is a type inconsistency,
// and the global FormData (expected by the fetch body) is actually not compliant
// with the Web spec and its TS declarations. formData.set doesn't exist, and
// formData.append doesn't support a Blob being passed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formData: any = new FormData();
formData.append("file", {
name: file.name,
type: file.type || mime.getType(file.name) || "application/octet-stream",
uri: file.uri,
} as unknown as Blob);

name: fileMetadata.name,
type:
fileMetadata.type ||
mime.getType(fileMetadata.name) ||
"application/octet-stream",
uri: fileMetadata.uri,
});
let response: Response;
try {
const response = await fetch(
`${process.env.EXPO_PUBLIC_WALLET_API}/wallet`,
response = await fetch(
new URL("wallet", process.env.EXPO_PUBLIC_WALLET_API),
{
method: "PUT",
body: formData,
}
);
} catch (e) {
console.debug("Resolving the file and uploading it to the wallet failed.");
throw e;
}

if (response.ok) {
console.debug(
`Uploaded file to Wallet. HTTP response status:${response.status}`
);
} else {
throw Error(
`Failed to upload file to Wallet. HTTP response status from Wallet Backend service:${
response.status
}`
);
}
} catch (error) {
throw Error("Failed to retrieve and upload file to Wallet", {
cause: error,
});
if (response.ok) {
console.debug(
`Uploaded file to Wallet. HTTP response status:${response.status}`
);
} else {
throw handleErrorResponse(
response,
await response.text(),
"Failed to upload file to Wallet"
);
}
};

Expand Down
11 changes: 10 additions & 1 deletion app/(tabs)/home/download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { RDF_CONTENT_TYPE } from "@/utils/constants";
import type { WalletFile } from "@/types/WalletFile";
import { isDownloadQR } from "@/types/accessPrompt";
import { useError } from "@/hooks/useError";
import { hasProblemDetails } from "@inrupt/solid-client-errors";

interface FileDetailProps {
file: WalletFile;
Expand All @@ -52,7 +53,15 @@ const Page: React.FC<FileDetailProps> = () => {
await queryClient.invalidateQueries({ queryKey: ["files"] });
},
onError: (error) => {
console.warn(error);
if (hasProblemDetails(error)) {
console.debug(
`${error.problemDetails.status}: ${error.problemDetails.title}.`
);
console.debug(error.problemDetails.detail);
} else {
console.debug("A non-HTTP error happened.");
console.debug(error);
}
showErrorMsg("Unable to save the file into your Wallet.");
},
mutationKey: ["filesMutation"],
Expand Down
1 change: 1 addition & 0 deletions app/access-prompt/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function mockUseQuery(
): ReturnType<typeof ReactQuery.useQuery> {
return {
data,
error: null,
isLoading: false,
isFetching: false,
refetch: jest.fn<ReturnType<typeof ReactQuery.useQuery>["refetch"]>(),
Expand Down
43 changes: 26 additions & 17 deletions app/access-prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
import CardInfo from "@/components/common/CardInfo";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
import Loading from "@/components/LoadingButton";
import { hasProblemDetails } from "@inrupt/solid-client-errors";

const Page: React.FC = () => {
const params = useLocalSearchParams();
Expand All @@ -42,14 +43,16 @@ const Page: React.FC = () => {
);
}
const router = useRouter();
const { data, isLoading, isFetching } = useQuery<AccessPromptResource>({
queryKey: ["accessPromptResource"],
queryFn: () =>
getAccessPromptResource({
type: params.type,
webId: params.webId,
}),
});
const { data, error, isLoading, isFetching } = useQuery<AccessPromptResource>(
{
queryKey: ["accessPromptResource"],
queryFn: () =>
getAccessPromptResource({
type: params.type,
webId: params.webId,
}),
}
);
const navigation = useNavigation();
const mutation = useMutation({
mutationFn: requestAccessPrompt,
Expand Down Expand Up @@ -90,23 +93,29 @@ const Page: React.FC = () => {
});
};

if (isLoading || isFetching)
return (
<View style={styles.container}>
<Loading isLoading={true} />
</View>
);

if (!data) {
if (
error !== null &&
hasProblemDetails(error) &&
error.problemDetails.status === 404
) {
return (
<View style={styles.emptyState} testID="no-prompts">
<View style={styles.emptyState}>
<ThemedText style={styles.emptyStateText}>
Resource not found
</ThemedText>
</View>
);
}

if (isLoading || isFetching)
return (
<View style={styles.container}>
<Loading isLoading={true} />
</View>
);

if (!data) return <View style={styles.container} testID="no-prompts" />;

return (
<View style={styles.container}>
<View style={styles.content}>
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@gorhom/bottom-sheet": "^4.6.4",
"@inrupt/solid-client-errors": "^0.0.2",
"@react-native-cookies/cookies": "^6.2.1",
"@react-navigation/native": "^6.1.18",
"@tanstack/react-query": "^5.51.23",
Expand Down

0 comments on commit a52ff1a

Please sign in to comment.