Adding Retry Interceptor for Failed Requests and Endpoint Display (#679)

This pull request introduces a new feature that enhances the error
handling and user experience in our React application. It implements an
interceptor to automatically retry failed requests and provides a clear
display of the failing endpoint to the user.

Changes Made:

Added a new interceptor to the Axios HTTP client in our React
application.
The interceptor detects failed requests and automatically retries them
based on a predefined retry count.
When a request fails, the interceptor captures the failing endpoint and
stores it.
Modified the user interface to display a user-friendly error message
indicating the failing endpoint.
Enhanced error handling to provide detailed information and better user
feedback.
This commit is contained in:
Lucas Oliveira 2023-07-25 10:08:50 -03:00 • committed by GitHub
commit c009602b57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 121 additions and 67 deletions

View file

@ -29,6 +29,7 @@
"@tabler/icons-react": "^2.18.0", "@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2", "accordion": "^3.0.2",
"ace-builds": "^1.16.0", "ace-builds": "^1.16.0",
"add": "^2.0.6", "add": "^2.0.6",
@ -142,6 +143,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
"integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
"version": "7.22.9", "version": "7.22.9",
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View file

@ -24,6 +24,7 @@
"@tabler/icons-react": "^2.18.0", "@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2", "accordion": "^3.0.2",
"ace-builds": "^1.16.0", "ace-builds": "^1.16.0",
"add": "^2.0.6", "add": "^2.0.6",

View file

@ -56,4 +56,4 @@ const AccordionContent = React.forwardRef<
)); ));
AccordionContent.displayName = AccordionPrimitive.Content.displayName; AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

View file

@ -77,9 +77,9 @@ CardFooter.displayName = "CardFooter";
export { export {
Card, Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent, CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
}; };

View file

@ -116,10 +116,10 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
DialogTrigger,
DialogContent, DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription, DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
}; };

View file

@ -182,18 +182,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioItem, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuTrigger,
}; };

View file

@ -218,19 +218,19 @@ MenubarShortcut.displayname = "MenubarShortcut";
export { export {
Menubar, Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem, MenubarCheckboxItem,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarLabel,
MenubarMenu,
MenubarPortal,
MenubarRadioGroup, MenubarRadioGroup,
MenubarRadioItem, MenubarRadioItem,
MenubarPortal, MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent, MenubarSubContent,
MenubarSubTrigger, MenubarSubTrigger,
MenubarGroup, MenubarTrigger,
MenubarSub,
MenubarShortcut,
}; };

View file

@ -103,11 +103,11 @@ TableCaption.displayName = "TableCaption";
export { export {
Table, Table,
TableHeader,
TableBody, TableBody,
TableCaption,
TableCell,
TableFooter, TableFooter,
TableHead, TableHead,
TableHeader,
TableRow, TableRow,
TableCell,
TableCaption,
}; };

View file

@ -51,4 +51,4 @@ const TabsContent = React.forwardRef<
)); ));
TabsContent.displayName = TabsPrimitive.Content.displayName; TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }; export { Tabs, TabsContent, TabsList, TabsTrigger };

View file

@ -28,4 +28,4 @@ const TooltipContent = React.forwardRef<
)); ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName; TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View file

@ -71,18 +71,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Clear the interval if successful. // Clear the interval if successful.
clearInterval(intervalId); clearInterval(intervalId);
} catch (error) { } catch (error) {
retryCount++; console.error("An error has occurred while fetching types.");
// On error, double the delay for the next attempt up to a maximum.
delay = Math.min(30000, delay * 2);
// Log errors but don't do anything else - the function will try again on the next interval.
console.error(error);
// Clear the old interval and start a new one with the new delay.
if (retryCount <= maxRetryCount) {
clearInterval(intervalId);
intervalId = setInterval(getTypes, delay);
} else {
console.error("Max retry attempts reached. Stopping retries.");
}
} }
} }

View file

@ -0,0 +1,58 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect, useRef } from "react";
import { alertContext } from "../../contexts/alertContext";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
baseURL: "",
});
function ApiInterceptor() {
const retryCounts = useRef([]);
const { setErrorData } = useContext(alertContext);
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
let retryCount = 0;
while (retryCount < 4) {
await sleep(5000); // Sleep for 5 seconds
retryCount++;
try {
const response = await axios.request(error.config);
return response;
} catch (error) {
if (retryCount === 3) {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
"Refresh the page",
"Use a new flow tab",
"Check if the backend is up",
"Endpoint: " + error.config.url,
],
});
return Promise.reject(error);
}
}
}
}
);
return () => {
// Clean up the interceptor when the component unmounts
api.interceptors.response.eject(interceptor);
};
}, [retryCounts]);
return null;
}
// Function to sleep for a given duration in milliseconds
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export { ApiInterceptor, api };

View file

@ -1,5 +1,6 @@
import axios, { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow"; import { ReactFlowJsonObject } from "reactflow";
import { api } from "../../controllers/API/api";
import { APIObjectType, sendAllProps } from "../../types/api/index"; import { APIObjectType, sendAllProps } from "../../types/api/index";
import { FlowStyleType, FlowType } from "../../types/flow"; import { FlowStyleType, FlowType } from "../../types/flow";
import { import {
@ -17,16 +18,14 @@ import {
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects. * @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
*/ */
export async function getAll(): Promise<AxiosResponse<APIObjectType>> { export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
return await axios.get(`/api/v1/all`); return await api.get(`/api/v1/all`);
} }
const GITHUB_API_URL = "https://api.github.com"; const GITHUB_API_URL = "https://api.github.com";
export async function getRepoStars(owner, repo) { export async function getRepoStars(owner, repo) {
try { try {
const response = await axios.get( const response = await api.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`);
`${GITHUB_API_URL}/repos/${owner}/${repo}`
);
return response.data.stargazers_count; return response.data.stargazers_count;
} catch (error) { } catch (error) {
console.error("Error fetching repository data:", error); console.error("Error fetching repository data:", error);
@ -41,13 +40,13 @@ export async function getRepoStars(owner, repo) {
* @returns {AxiosResponse<any>} The API response. * @returns {AxiosResponse<any>} The API response.
*/ */
export async function sendAll(data: sendAllProps) { export async function sendAll(data: sendAllProps) {
return await axios.post(`/api/v1/predict`, data); return await api.post(`/api/v1/predict`, data);
} }
export async function postValidateCode( export async function postValidateCode(
code: string code: string
): Promise<AxiosResponse<errorsTypeAPI>> { ): Promise<AxiosResponse<errorsTypeAPI>> {
return await axios.post("/api/v1/validate/code", { code }); return await api.post("/api/v1/validate/code", { code });
} }
/** /**
@ -62,7 +61,7 @@ export async function postValidatePrompt(
template: string, template: string,
frontend_node: APIClassType frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> { ): Promise<AxiosResponse<PromptTypeAPI>> {
return await axios.post("/api/v1/validate/prompt", { return await api.post("/api/v1/validate/prompt", {
name: name, name: name,
template: template, template: template,
frontend_node: frontend_node, frontend_node: frontend_node,
@ -77,14 +76,14 @@ export async function postValidatePrompt(
export async function getExamples(): Promise<FlowType[]> { export async function getExamples(): Promise<FlowType[]> {
const url = const url =
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples?ref=main"; "https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples?ref=main";
const response = await axios.get(url); const response = await api.get(url);
const jsonFiles = response.data.filter((file: any) => { const jsonFiles = response.data.filter((file: any) => {
return file.name.endsWith(".json"); return file.name.endsWith(".json");
}); });
const contentsPromises = jsonFiles.map(async (file: any) => { const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await axios.get(file.download_url); const contentResponse = await api.get(file.download_url);
return contentResponse.data; return contentResponse.data;
}); });
@ -106,11 +105,12 @@ export async function saveFlowToDatabase(newFlow: {
style?: FlowStyleType; style?: FlowStyleType;
}): Promise<FlowType> { }): Promise<FlowType> {
try { try {
const response = await axios.post("/api/v1/flows/", { const response = await api.post("/api/v1/flows/", {
name: newFlow.name, name: newFlow.name,
data: newFlow.data, data: newFlow.data,
description: newFlow.description, description: newFlow.description,
}); });
if (response.status !== 201) { if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -131,7 +131,7 @@ export async function updateFlowInDatabase(
updatedFlow: FlowType updatedFlow: FlowType
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, { const response = await api.patch(`/api/v1/flows/${updatedFlow.id}`, {
name: updatedFlow.name, name: updatedFlow.name,
data: updatedFlow.data, data: updatedFlow.data,
description: updatedFlow.description, description: updatedFlow.description,
@ -155,7 +155,7 @@ export async function updateFlowInDatabase(
*/ */
export async function readFlowsFromDatabase() { export async function readFlowsFromDatabase() {
try { try {
const response = await axios.get("/api/v1/flows/"); const response = await api.get("/api/v1/flows/");
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -168,7 +168,7 @@ export async function readFlowsFromDatabase() {
export async function downloadFlowsFromDatabase() { export async function downloadFlowsFromDatabase() {
try { try {
const response = await axios.get("/api/v1/flows/download/"); const response = await api.get("/api/v1/flows/download/");
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -181,7 +181,7 @@ export async function downloadFlowsFromDatabase() {
export async function uploadFlowsToDatabase(flows) { export async function uploadFlowsToDatabase(flows) {
try { try {
const response = await axios.post(`/api/v1/flows/upload/`, flows); const response = await api.post(`/api/v1/flows/upload/`, flows);
if (response.status !== 201) { if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
@ -202,7 +202,7 @@ export async function uploadFlowsToDatabase(flows) {
*/ */
export async function deleteFlowFromDatabase(flowId: string) { export async function deleteFlowFromDatabase(flowId: string) {
try { try {
const response = await axios.delete(`/api/v1/flows/${flowId}`); const response = await api.delete(`/api/v1/flows/${flowId}`);
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -222,7 +222,7 @@ export async function deleteFlowFromDatabase(flowId: string) {
*/ */
export async function getFlowFromDatabase(flowId: number) { export async function getFlowFromDatabase(flowId: number) {
try { try {
const response = await axios.get(`/api/v1/flows/${flowId}`); const response = await api.get(`/api/v1/flows/${flowId}`);
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -241,7 +241,7 @@ export async function getFlowFromDatabase(flowId: number) {
*/ */
export async function getFlowStylesFromDatabase() { export async function getFlowStylesFromDatabase() {
try { try {
const response = await axios.get("/api/v1/flow_styles/"); const response = await api.get("/api/v1/flow_styles/");
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -261,7 +261,7 @@ export async function getFlowStylesFromDatabase() {
*/ */
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) { export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
try { try {
const response = await axios.post("/api/v1/flow_styles/", flowStyle, { const response = await api.post("/api/v1/flow_styles/", flowStyle, {
headers: { headers: {
accept: "application/json", accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -284,7 +284,7 @@ export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information. * @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information.
*/ */
export async function getVersion() { export async function getVersion() {
const respnose = await axios.get("/api/v1/version"); const respnose = await api.get("/api/v1/version");
return respnose.data; return respnose.data;
} }
@ -294,7 +294,7 @@ export async function getVersion() {
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status. * @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status.
*/ */
export async function getHealth() { export async function getHealth() {
return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1 return await api.get("/health"); // Health is the only endpoint that doesn't require /api/v1
} }
/** /**
@ -306,7 +306,7 @@ export async function getHealth() {
export async function getBuildStatus( export async function getBuildStatus(
flowId: string flowId: string
): Promise<BuildStatusTypeAPI> { ): Promise<BuildStatusTypeAPI> {
return await axios.get(`/api/v1/build/${flowId}/status`); return await api.get(`/api/v1/build/${flowId}/status`);
} }
//docs for postbuildinit //docs for postbuildinit
@ -319,7 +319,7 @@ export async function getBuildStatus(
export async function postBuildInit( export async function postBuildInit(
flow: FlowType flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> { ): Promise<AxiosResponse<InitTypeAPI>> {
return await axios.post(`/api/v1/build/init/${flow.id}`, flow); return await api.post(`/api/v1/build/init/${flow.id}`, flow);
} }
// fetch(`/upload/${id}`, { // fetch(`/upload/${id}`, {
@ -337,5 +337,5 @@ export async function uploadFile(
): Promise<AxiosResponse<UploadFileTypeAPI>> { ): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
return await axios.post(`/api/v1/upload/${id}`, formData); return await api.post(`/api/v1/upload/${id}`, formData);
} }

View file

@ -4,6 +4,7 @@ import App from "./App";
import ContextWrapper from "./contexts"; import ContextWrapper from "./contexts";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import { ApiInterceptor } from "./controllers/API/api";
import "./index.css"; import "./index.css";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
@ -13,6 +14,7 @@ root.render(
<ContextWrapper> <ContextWrapper>
<BrowserRouter> <BrowserRouter>
<App /> <App />
<ApiInterceptor />
</BrowserRouter> </BrowserRouter>
</ContextWrapper> </ContextWrapper>
); );