Просмотр исходного кода

[ui-storagebrowser] implements error handling (#3932)

* [ui-storagebrowser] implements error handling

* makes error messages consistent

* fixes the error message and toaster
Ram Prasad Agarwal 1 год назад
Родитель
Сommit
4147e6bcda

+ 7 - 2
desktop/core/src/desktop/js/api/utils.ts

@@ -47,6 +47,7 @@ export interface ApiFetchOptions<T, E = string> extends AxiosRequestConfig {
   ignoreSuccessErrors?: boolean;
   transformResponse?: AxiosResponseTransformer;
   qsEncodeData?: boolean;
+  isRawError?: boolean;
   handleSuccess?: (
     response: T & DefaultApiResponse,
     resolve: (val: T) => void,
@@ -156,10 +157,14 @@ const notifyError = <T>(
 const handleErrorResponse = <T>(
   err: AxiosError<DefaultApiResponse>,
   reject: (reason?: unknown) => void,
-  options?: Pick<ApiFetchOptions<T>, 'silenceErrors'>
+  options?: Pick<ApiFetchOptions<T>, 'silenceErrors' | 'isRawError'>
 ): void => {
   const errorMessage = extractErrorMessage(err.response && err.response.data);
-  reject(errorMessage);
+  if (options?.isRawError) {
+    reject(err);
+  } else {
+    reject(errorMessage);
+  }
   notifyError(errorMessage, (err && err.response) || err, options);
 };
 

+ 19 - 12
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage.tsx

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import React from 'react';
-import { Tabs, Spin } from 'antd';
+import { Tabs } from 'antd';
 
 import DataBrowserIcon from '@cloudera/cuix-core/icons/react/DataBrowserIcon';
 
@@ -26,31 +26,38 @@ import { ApiFileSystem, FILESYSTEMS_API_URL } from '../../reactComponents/FileCh
 
 import './StorageBrowserPage.scss';
 import useLoadData from '../../utils/hooks/useLoadData';
+import LoadingErrorWrapper from '../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 const StorageBrowserPage = (): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { data: fileSystems, loading } = useLoadData<ApiFileSystem[]>(FILESYSTEMS_API_URL);
+  const { data, loading, error, reloadData } = useLoadData<ApiFileSystem[]>(FILESYSTEMS_API_URL);
+
+  const errorConfig = [
+    {
+      enabled: !!error,
+      message: t('An error occurred while fetching the filesystem'),
+      action: t('Retry'),
+      onClick: reloadData
+    }
+  ];
 
   return (
     <div className="hue-storage-browser cuix antd">
       <CommonHeader title={t('Storage Browser')} icon={<DataBrowserIcon />} />
-      <Spin spinning={loading}>
+      <LoadingErrorWrapper loading={loading} errors={errorConfig}>
         <Tabs
           className="hue-storage-browser__tab"
-          defaultActiveKey="0"
-          items={fileSystems?.map(system => ({
-            label: system.file_system.toUpperCase(),
-            key: system.file_system + '_tab',
+          defaultActiveKey={data?.[0]?.file_system}
+          items={data?.map(fs => ({
+            label: fs.file_system.toUpperCase(),
+            key: fs.file_system,
             children: (
-              <StorageBrowserTab
-                homeDir={system.user_home_directory}
-                fileSystem={system.file_system}
-              />
+              <StorageBrowserTab homeDir={fs.user_home_directory} fileSystem={fs.file_system} />
             )
           }))}
         />
-      </Spin>
+      </LoadingErrorWrapper>
     </div>
   );
 };

+ 23 - 5
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx

@@ -15,7 +15,6 @@
 // limitations under the License.
 
 import React, { useEffect, useState } from 'react';
-import { Spin } from 'antd';
 
 import { i18nReact } from '../../../utils/i18nReact';
 import BucketIcon from '@cloudera/cuix-core/icons/react/BucketIcon';
@@ -29,6 +28,7 @@ import useLoadData from '../../../utils/hooks/useLoadData';
 import './StorageBrowserTab.scss';
 import StorageFilePage from '../StorageFilePage/StorageFilePage';
 import changeURL from '../../../utils/url/changeURL';
+import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 import { getFileSystemAndPath } from '../../../reactComponents/PathBrowser/PathBrowser.util';
 
 interface StorageBrowserTabProps {
@@ -58,6 +58,7 @@ const StorageBrowserTab = ({
   const {
     data: fileStats,
     loading,
+    error,
     reloadData
   } = useLoadData<FileStats>(FILE_STATS_API_URL, {
     params: { path: filePath },
@@ -75,8 +76,25 @@ const StorageBrowserTab = ({
     }
   }, [filePath, urlPathname, urlFilePath, window.location.pathname]);
 
+  const errorConfig = [
+    {
+      enabled: error?.response?.status === 404,
+      message: t('Error: Path "{{path}}" not found.', { path: filePath }),
+      action: t('Go to home directory'),
+      onClick: () => setFilePath(homeDir)
+    },
+    {
+      enabled: !!error && error?.response?.status !== 404,
+      message: t('An error occurred while fetching filesystem "{{fileSystem}}".', {
+        fileSystem: fileSystem.toUpperCase()
+      }),
+      action: t('Retry'),
+      onClick: reloadData
+    }
+  ];
+
   return (
-    <Spin spinning={loading}>
+    <LoadingErrorWrapper loading={loading} errors={errorConfig}>
       <div className="hue-storage-browser-tab-content" data-testid={testId}>
         <div className="hue-storage-browser__title-bar" data-testid={`${testId}-title-bar`}>
           <BucketIcon className="hue-storage-browser__icon" data-testid={`${testId}-icon`} />
@@ -96,14 +114,14 @@ const StorageBrowserTab = ({
             showIcon={false}
           />
         </div>
-        {fileStats?.type === BrowserViewType.dir && (
+        {fileStats?.type === BrowserViewType.dir && !loading && (
           <StorageDirectoryPage fileStats={fileStats} onFilePathChange={setFilePath} />
         )}
-        {fileStats?.type === BrowserViewType.file && (
+        {fileStats?.type === BrowserViewType.file && !loading && (
           <StorageFilePage fileName={fileName} fileStats={fileStats} onReload={reloadData} />
         )}
       </div>
-    </Spin>
+    </LoadingErrorWrapper>
   );
 };
 

+ 14 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx

@@ -16,7 +16,7 @@
 
 import React, { useMemo, useState, useCallback, useEffect } from 'react';
 import { ColumnProps } from 'antd/lib/table';
-import { Input, Spin, Tooltip } from 'antd';
+import { Input, Tooltip } from 'antd';
 
 import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon';
 import FileIcon from '@cloudera/cuix-core/icons/react/DocumentationIcon';
@@ -49,6 +49,7 @@ import DragAndDrop from '../../../reactComponents/DragAndDrop/DragAndDrop';
 import UUID from '../../../utils/string/UUID';
 import { UploadItem } from '../../../utils/hooks/useFileUpload/util';
 import FileUploadQueue from '../../../reactComponents/FileUploadQueue/FileUploadQueue';
+import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 interface StorageDirectoryPageProps {
   fileStats: FileStats;
@@ -88,6 +89,7 @@ const StorageDirectoryPage = ({
   const {
     data: filesData,
     loading: listDirectoryLoading,
+    error: listDirectoryError,
     reloadData
   } = useLoadData<ListDirectory>(LIST_DIRECTORY_API_URL, {
     params: {
@@ -262,6 +264,15 @@ const StorageDirectoryPage = ({
     emptyText: t('Folder is empty')
   };
 
+  const errorConfig = [
+    {
+      enabled: !!listDirectoryError,
+      message: t('An error occurred while fetching the data'),
+      action: t('Retry'),
+      onClick: reloadData
+    }
+  ];
+
   return (
     <>
       <div className="hue-storage-browser__actions-bar">
@@ -291,7 +302,7 @@ const StorageDirectoryPage = ({
       </div>
 
       <DragAndDrop onDrop={onFilesDrop}>
-        <Spin spinning={loadingFiles || listDirectoryLoading}>
+        <LoadingErrorWrapper loading={loadingFiles || listDirectoryLoading} errors={errorConfig}>
           <Table
             className={className}
             columns={getColumns(tableData[0] ?? {})}
@@ -320,7 +331,7 @@ const StorageDirectoryPage = ({
               pageStats={filesData?.page}
             />
           )}
-        </Spin>
+        </LoadingErrorWrapper>
       </DragAndDrop>
       {filesToUpload.length > 0 && (
         <FileUploadQueue

+ 118 - 106
desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx

@@ -36,9 +36,9 @@ import {
   SUPPORTED_FILE_EXTENSIONS,
   SupportedFileTypes
 } from '../../../utils/constants/storageBrowser';
-import { Spin } from 'antd';
 import useLoadData from '../../../utils/hooks/useLoadData';
 import { getLastKnownConfig } from '../../../config/hueConfig';
+import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 
 interface StorageFilePageProps {
   onReload: () => void;
@@ -57,20 +57,19 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
 
   const { loading: isSaving, save } = useSaveData(SAVE_FILE_API_URL);
 
-  const { data: fileData, loading: loadingPreview } = useLoadData<FilePreview>(
-    FILE_PREVIEW_API_URL,
-    {
-      params: {
-        path: fileStats.path
-      },
-      onSuccess: d => setFileContent(d.contents),
-      skip:
-        fileStats.path === '' ||
-        fileStats.path === undefined ||
-        fileStats?.type !== BrowserViewType.file ||
-        EDITABLE_FILE_FORMATS[fileType] === undefined
-    }
-  );
+  const {
+    data: fileData,
+    loading: loadingPreview,
+    error: errorPreview
+  } = useLoadData<FilePreview>(FILE_PREVIEW_API_URL, {
+    params: { path: fileStats.path },
+    onSuccess: d => setFileContent(d.contents),
+    skip:
+      fileStats.path === '' ||
+      fileStats.path === undefined ||
+      fileStats?.type !== BrowserViewType.file ||
+      EDITABLE_FILE_FORMATS[fileType] === undefined
+  });
 
   const fileMetaData = useMemo(() => getFileMetaData(t, fileStats), [t, fileStats]);
 
@@ -116,8 +115,19 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
     EDITABLE_FILE_FORMATS[fileType] &&
     fileData?.compression?.toLocaleLowerCase() === 'none';
 
+  const errorConfig = [
+    {
+      enabled: !!errorPreview,
+      message: t('An error occurred while fetching file content for path "{{path}}".', {
+        path: fileStats.path
+      }),
+      action: t('Retry'),
+      onClick: onReload
+    }
+  ];
+
   return (
-    <Spin spinning={loadingPreview || isSaving}>
+    <>
       <div className="hue-storage-file-page">
         <div className="meta-data">
           {fileMetaData.map((row, index) => (
@@ -132,106 +142,108 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
           ))}
         </div>
 
-        <div className="preview">
-          <div className="preview__title-bar">
-            {t('Content')}
-            <div className="preview__action-group">
-              {isEditingEnabled && (
-                <PrimaryButton
-                  data-testid="preview--edit--button"
-                  data-event=""
-                  onClick={handleEdit}
-                >
-                  {t('Edit')}
-                </PrimaryButton>
-              )}
-              {isEditing && (
-                <>
+        <LoadingErrorWrapper loading={loadingPreview || isSaving} errors={errorConfig}>
+          <div className="preview">
+            <div className="preview__title-bar">
+              {t('Content')}
+              <div className="preview__action-group">
+                {isEditingEnabled && (
                   <PrimaryButton
-                    data-testid="preview--save--button"
+                    data-testid="preview--edit--button"
                     data-event=""
-                    onClick={handleSave}
-                    disabled={fileContent === fileData?.contents}
+                    onClick={handleEdit}
                   >
-                    {t('Save')}
+                    {t('Edit')}
                   </PrimaryButton>
-                  <Button
-                    role="button"
-                    data-testid="preview--cancel--button"
-                    data-event=""
-                    onClick={handleCancel}
-                  >
-                    {t('Cancel')}
-                  </Button>
-                </>
-              )}
-              {config?.storage_browser.enable_file_download_button && (
-                <a href={`${DOWNLOAD_API_URL}${fileStats.path}`}>
-                  <PrimaryButton
-                    data-testid="preview--download--button"
-                    data-event=""
-                    onClick={handleDownload}
-                  >
-                    {t('Download')}
-                  </PrimaryButton>
-                </a>
-              )}
+                )}
+                {isEditing && (
+                  <>
+                    <PrimaryButton
+                      data-testid="preview--save--button"
+                      data-event=""
+                      onClick={handleSave}
+                      disabled={fileContent === fileData?.contents}
+                    >
+                      {t('Save')}
+                    </PrimaryButton>
+                    <Button
+                      role="button"
+                      data-testid="preview--cancel--button"
+                      data-event=""
+                      onClick={handleCancel}
+                    >
+                      {t('Cancel')}
+                    </Button>
+                  </>
+                )}
+                {config?.storage_browser.enable_file_download_button && (
+                  <a href={`${DOWNLOAD_API_URL}${fileStats.path}`}>
+                    <PrimaryButton
+                      data-testid="preview--download--button"
+                      data-event=""
+                      onClick={handleDownload}
+                    >
+                      {t('Download')}
+                    </PrimaryButton>
+                  </a>
+                )}
+              </div>
             </div>
-          </div>
 
-          <div className="preview__content">
-            {fileType === SupportedFileTypes.TEXT && (
-              <textarea
-                value={fileContent}
-                onChange={e => setFileContent(e.target.value)}
-                readOnly={!isEditing}
-                className="preview__textarea"
-              />
-            )}
-
-            {fileType === SupportedFileTypes.IMAGE && <img src={filePreviewUrl} alt={fileName} />}
-
-            {fileType === SupportedFileTypes.DOCUMENT && (
-              <div className="preview__document">
-                <div>
-                  <PrimaryButton
-                    data-testid=""
-                    data-event=""
-                    onClick={() => window.open(filePreviewUrl)}
-                  >
-                    {t('Preview document')}
-                  </PrimaryButton>
+            <div className="preview__content">
+              {fileType === SupportedFileTypes.TEXT && (
+                <textarea
+                  value={fileContent}
+                  onChange={e => setFileContent(e.target.value)}
+                  readOnly={!isEditing}
+                  className="preview__textarea"
+                />
+              )}
+
+              {fileType === SupportedFileTypes.IMAGE && <img src={filePreviewUrl} alt={fileName} />}
+
+              {fileType === SupportedFileTypes.DOCUMENT && (
+                <div className="preview__document">
+                  <div>
+                    <PrimaryButton
+                      data-testid=""
+                      data-event=""
+                      onClick={() => window.open(filePreviewUrl)}
+                    >
+                      {t('Preview document')}
+                    </PrimaryButton>
+                  </div>
+                  <div>{t('The Document will open in a new tab.')}</div>
                 </div>
-                <div>{t('The Document will open in a new tab.')}</div>
-              </div>
-            )}
-
-            {fileType === SupportedFileTypes.AUDIO && (
-              <audio controls preload="auto" data-testid="preview__content__audio">
-                <source src={filePreviewUrl} />
-                {t('Your browser does not support the audio element.')}
-              </audio>
-            )}
-
-            {fileType === SupportedFileTypes.VIDEO && (
-              <video controls preload="auto" data-testid="preview__content__video">
-                <source src={filePreviewUrl} />
-                {t('Your browser does not support the video element.')}
-              </video>
-            )}
-
-            {fileType === SupportedFileTypes.OTHER && (
-              <div className="preview__unsupported">
-                {t('Preview not available for this file. Please download the file to view.')}
-                <br />
-                {t(`Supported file extensions: 
+              )}
+
+              {fileType === SupportedFileTypes.AUDIO && (
+                <audio controls preload="auto" data-testid="preview__content__audio">
+                  <source src={filePreviewUrl} />
+                  {t('Your browser does not support the audio element.')}
+                </audio>
+              )}
+
+              {fileType === SupportedFileTypes.VIDEO && (
+                <video controls preload="auto" data-testid="preview__content__video">
+                  <source src={filePreviewUrl} />
+                  {t('Your browser does not support the video element.')}
+                </video>
+              )}
+
+              {fileType === SupportedFileTypes.OTHER && (
+                <div className="preview__unsupported">
+                  {t('Preview not available for this file. Please download the file to view.')}
+                  <br />
+                  {t(`Supported file extensions: 
                 ${Object.keys(SUPPORTED_FILE_EXTENSIONS).join(', ')}`)}
-              </div>
-            )}
+                </div>
+              )}
+            </div>
           </div>
-        </div>
+        </LoadingErrorWrapper>
       </div>
-    </Spin>
+    </>
   );
 };
 

+ 24 - 0
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.scss

@@ -0,0 +1,24 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+.loading-error-wrapper__error {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  gap: 16px;
+}

+ 72 - 0
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.test.tsx

@@ -0,0 +1,72 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import LoadingErrorWrapper from './LoadingErrorWrapper';
+
+describe('LoadingErrorWrapper', () => {
+  const defaultProps = {
+    loading: false,
+    errors: [],
+    children: <div>Children Content</div>
+  };
+
+  it('should render loading spinner when loading is true', () => {
+    const { getAllByTestId, queryByText } = render(
+      <LoadingErrorWrapper {...defaultProps} loading={true} />
+    );
+
+    expect(getAllByTestId('loading-error-wrapper__sppiner')).toHaveLength(2);
+    expect(queryByText('Children Content')).toBeInTheDocument();
+  });
+
+  it('should render children when loading is false and no errors', () => {
+    const { getByText } = render(<LoadingErrorWrapper {...defaultProps} loading={false} />);
+
+    expect(getByText('Children Content')).toBeInTheDocument();
+  });
+
+  it('should render error messages when there are enabled errors', () => {
+    const errors = [
+      { enabled: true, message: 'Error 1' },
+      { enabled: false, message: 'Error 2' }
+    ];
+
+    const { getByText, queryByText } = render(
+      <LoadingErrorWrapper {...defaultProps} errors={errors} loading={false} />
+    );
+
+    expect(getByText('Error 1')).toBeInTheDocument();
+    expect(queryByText('Error 2')).not.toBeInTheDocument();
+  });
+
+  it('should render action button for errors with onClick', () => {
+    const mockOnClick = jest.fn();
+    const errors = [
+      { enabled: true, message: 'Error with action', action: 'Retry', onClick: mockOnClick }
+    ];
+
+    const { getByText } = render(
+      <LoadingErrorWrapper {...defaultProps} errors={errors} loading={false} />
+    );
+
+    const button = getByText('Retry');
+    expect(button).toBeInTheDocument();
+
+    fireEvent.click(button);
+    expect(mockOnClick).toHaveBeenCalled();
+  });
+
+  it('should render multiple errors if there are multiple enabled errors', () => {
+    const errors = [
+      { enabled: true, message: 'Error 1' },
+      { enabled: true, message: 'Error 2' }
+    ];
+
+    const { getByText } = render(
+      <LoadingErrorWrapper {...defaultProps} errors={errors} loading={false} />
+    );
+
+    expect(getByText('Error 1')).toBeInTheDocument();
+    expect(getByText('Error 2')).toBeInTheDocument();
+  });
+});

+ 75 - 0
desktop/core/src/desktop/js/reactComponents/LoadingErrorWrapper/LoadingErrorWrapper.tsx

@@ -0,0 +1,75 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Spin } from 'antd';
+import { PrimaryButton } from 'cuix/dist/components/Button';
+import React from 'react';
+import './LoadingErrorWrapper.scss';
+
+interface WrapperError {
+  enabled: boolean;
+  message: string;
+  action?: string;
+  onClick?: () => void;
+}
+
+interface LoadingErrorWrapperProps {
+  loading: boolean;
+  errors: WrapperError[];
+  children: React.ReactNode;
+}
+
+const LoadingErrorWrapper = ({
+  loading,
+  errors,
+  children
+}: LoadingErrorWrapperProps): JSX.Element => {
+  if (loading) {
+    // TODO: discuss in UI meeting, if we need to render children while loading
+    // Initial thoughts:
+    // -- On first render -> hide children
+    // -- On re-render -> render children
+    return (
+      <Spin spinning={loading} data-testid="loading-error-wrapper__sppiner">
+        {children}
+      </Spin>
+    );
+  }
+
+  const enabledErrors = errors.filter(error => error.enabled);
+  if (enabledErrors.length > 0) {
+    return (
+      <>
+        {enabledErrors
+          .filter(error => error.enabled)
+          .map(error => (
+            <div className="loading-error-wrapper__error" key={error.message}>
+              <div>{error.message}</div>
+              {error.onClick && (
+                <PrimaryButton onClick={error.onClick} data-event="">
+                  {error.action}
+                </PrimaryButton>
+              )}
+            </div>
+          ))}
+      </>
+    );
+  }
+
+  return <>{children}</>;
+};
+
+export default LoadingErrorWrapper;

+ 10 - 8
desktop/core/src/desktop/js/utils/hooks/useLoadData.ts

@@ -16,20 +16,21 @@
 
 import { useCallback, useEffect, useMemo, useState } from 'react';
 import { ApiFetchOptions, get } from '../../api/utils';
+import { AxiosError } from 'axios';
 
 export interface Options<T, U> {
   params?: U;
   fetchOptions?: ApiFetchOptions<T>;
   skip?: boolean;
   onSuccess?: (data: T) => void;
-  onError?: (error: Error) => void;
+  onError?: (error: AxiosError) => void;
   pollInterval?: number;
 }
 
 interface UseLoadDataProps<T> {
   data?: T;
   loading: boolean;
-  error?: Error;
+  error?: AxiosError;
   reloadData: () => void;
 }
 
@@ -38,13 +39,14 @@ const useLoadData = <T, U = unknown>(
   options?: Options<T, U>
 ): UseLoadDataProps<T> => {
   const [localOptions, setLocalOptions] = useState<Options<T, U> | undefined>(options);
-  const [data, setData] = useState<T | undefined>();
+  const [data, setData] = useState<T>();
   const [loading, setLoading] = useState<boolean>(false);
-  const [error, setError] = useState<Error | undefined>();
+  const [error, setError] = useState<AxiosError>();
 
   const fetchOptionsDefault: ApiFetchOptions<T> = {
-    silenceErrors: false,
-    ignoreSuccessErrors: true
+    silenceErrors: true,
+    ignoreSuccessErrors: true,
+    isRawError: true
   };
 
   const fetchOptions = useMemo(
@@ -69,9 +71,9 @@ const useLoadData = <T, U = unknown>(
           localOptions.onSuccess(response);
         }
       } catch (error) {
-        setError(error instanceof Error ? error : new Error(error));
+        setError(error as AxiosError);
         if (localOptions?.onError) {
-          localOptions.onError(error);
+          localOptions.onError(error as AxiosError);
         }
       } finally {
         setLoading(false);