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.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [ ] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Roberto Dip 2024-06-14 18:39:03 -03:00 committed by GitHub
parent 7fb3d12477
commit 48ea95385e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 51 additions and 9 deletions

View file

@ -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)

View file

@ -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();
}

View file

@ -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;
};

View file

@ -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) => {

View file

@ -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,