From 48ea95385e7cdeee482bb11c01cf9297c3d04e57 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Fri, 14 Jun 2024 18:39:03 -0300 Subject: [PATCH] final adjustments to return timeout errors for software (#19772) for #19453 ![image](https://github.com/fleetdm/fleet/assets/4419992/c9849cf4-ea78-4393-8693-8ff8ea612221) I don't know how to test this in the server without a significant refactor. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality --- cmd/fleet/serve.go | 10 +++++++++- .../AddSoftwareModal/AddSoftwareModal.tsx | 14 ++++++++------ .../components/AddSoftwareModal/helpers.ts | 13 +++++++++++++ frontend/services/entities/software.ts | 15 +++++++++++++-- server/service/software_installers.go | 8 ++++++++ 5 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index dd330d2e43..4616883d2f 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -74,6 +74,8 @@ import ( var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$") +const softwareInstallerUploadTimeout = 2 * time.Minute + type initializer interface { // Initialize is used to populate a datastore with // preloaded data @@ -1056,9 +1058,15 @@ the way that the Fleet server works. // the frontend times out waiting for the upload after 2 minutes, so // use that same timeout: // https://www.figma.com/design/oQl2oQUG0iRkUy0YOxc307/%2314921-Deploy-security-agents-to-macOS%2C-Windows%2C-and-Linux-hosts?node-id=773-18032&t=QjEU6tc73tddNSqn-0 - if err := rc.SetReadDeadline(time.Now().Add(2 * time.Minute)); err != nil { + if err := rc.SetReadDeadline(time.Now().Add(softwareInstallerUploadTimeout)); err != nil { level.Error(logger).Log("msg", "http middleware failed to override endpoint read timeout", "err", err) } + // the write timeout should be extended as well to give the server time to + // write a response body with the right error, otherwise the connection is + // terminated abruptly. + if err := rc.SetWriteDeadline(time.Now().Add(softwareInstallerUploadTimeout + 30*time.Second)); err != nil { + level.Error(logger).Log("msg", "http middleware failed to override endpoint write timeout", "err", err) + } req.Body = http.MaxBytesReader(rw, req.Body, service.MaxSoftwareInstallerSize) } apiHandler.ServeHTTP(rw, req) diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx b/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx index 7aa3c39e63..3f83cac725 100644 --- a/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx +++ b/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx @@ -1,21 +1,23 @@ import React, { useContext, useEffect, useState } from "react"; import { InjectedRouter } from "react-router"; +import { AxiosResponse } from "axios"; import PATHS from "router/paths"; -import team, { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team"; -import { getErrorReason } from "interfaces/errors"; +import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team"; import softwareAPI from "services/entities/software"; import { NotificationContext } from "context/notification"; import { QueryParams, buildQueryStringFromParams } from "utilities/url"; +import { IApiError } from "interfaces/errors"; import Modal from "components/Modal"; import Button from "components/buttons/Button"; import AddSoftwareForm from "../AddSoftwareForm"; import { IAddSoftwareFormData } from "../AddSoftwareForm/AddSoftwareForm"; +import { getErrorMessage } from "./helpers"; -// 2 minutes -const UPLOAD_TIMEOUT = 120000; +// 2 minutes + 15 seconds to account for extra roundtrip time. +const UPLOAD_TIMEOUT = (2 * 60 + 15) * 1000; const MAX_FILE_SIZE_MB = 500; const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; @@ -97,7 +99,7 @@ const AddSoftwareModal = ({ // TODO: confirm we are deleting the second sentence (not modifying it) for non-self-service installers try { - await softwareAPI.addSoftwarePackage(formData, teamId); + await softwareAPI.addSoftwarePackage(formData, teamId, UPLOAD_TIMEOUT); renderFlash( "success", <> @@ -120,7 +122,7 @@ const AddSoftwareModal = ({ `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}` ); } catch (e) { - renderFlash("error", getErrorReason(e)); + renderFlash("error", getErrorMessage(e)); onExit(); } diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts b/frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts new file mode 100644 index 0000000000..bf6bef64a8 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts @@ -0,0 +1,13 @@ +import { getErrorReason } from "interfaces/errors"; + +const UPLOAD_ERROR_MESSAGES = { + default: { + message: "Couldn't add. Please try again.", + }, +}; + +// eslint-disable-next-line import/prefer-default-export +export const getErrorMessage = (err: unknown) => { + if (typeof err === "string") return err; + return getErrorReason(err) || UPLOAD_ERROR_MESSAGES.default.message; +}; diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index 3b4067dd7d..372b8b5268 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -191,7 +191,11 @@ export default { return sendRequest("GET", path); }, - addSoftwarePackage: (data: IAddSoftwareFormData, teamId?: number) => { + addSoftwarePackage: ( + data: IAddSoftwareFormData, + teamId?: number, + timeout?: number + ) => { const { SOFTWARE_PACKAGE_ADD } = endpoints; if (!data.software) { @@ -208,7 +212,14 @@ export default { formData.append("post_install_script", data.postInstallScript); teamId && formData.append("team_id", teamId.toString()); - return sendRequest("POST", SOFTWARE_PACKAGE_ADD, formData); + return sendRequest( + "POST", + SOFTWARE_PACKAGE_ADD, + formData, + undefined, + timeout, + true + ); }, deleteSoftwarePackage: (softwareId: number, teamId: number) => { diff --git a/server/service/software_installers.go b/server/service/software_installers.go index d22100a4ef..4b71f1ca09 100644 --- a/server/service/software_installers.go +++ b/server/service/software_installers.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "mime/multipart" + "net" "net/http" "strconv" @@ -47,6 +48,13 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http InternalErr: err, } } + var nerr net.Error + if errors.As(err, &nerr) && nerr.Timeout() { + return nil, fleet.NewUserMessageError( + ctxerr.New(ctx, "Couldn't upload. Please ensure your internet connection speed is sufficient and stable."), + http.StatusRequestTimeout, + ) + } return nil, &fleet.BadRequestError{ Message: "failed to parse multipart form: " + err.Error(), InternalErr: err,