function multiObjectDelete(authInfo, request, log, callback) {
    log.debug('processing request', { method: 'multiObjectDelete' });
    if (!request.post) {
        monitoring.promMetrics('DELETE', request.bucketName, 400,
            'multiObjectDelete');
        return callback(errors.MissingRequestBodyError);
    }
    const md5 = crypto.createHash('md5')
        .update(request.post, 'utf8').digest('base64');
    if (md5 !== request.headers['content-md5']) {
        monitoring.promMetrics('DELETE', request.bucketName, 400,
            'multiObjectDelete');
        return callback(errors.BadDigest);
    }

    const inPlayInternal = [];
    const bucketName = request.bucketName;
    const canonicalID = authInfo.getCanonicalID();
    const ip = requestUtils.getClientIp(request, config);
    const isSecure = requestUtils.getHttpProtocolSecurity(request, config);

    return async.waterfall([
        function parseXML(next) {
            return _parseXml(request.post,
                (err, quietSetting, objects) => {
                    if (err || objects.length < 1 || objects.length > 1000) {
                        return next(errors.MalformedXML);
                    }
                    return next(null, quietSetting, objects);
                });
        },
        function checkBucketMetadata(quietSetting, objects, next) {
            const errorResults = [];
            return metadata.getBucket(bucketName, log, (err, bucketMD) => {
                if (err) {
                    log.trace('error retrieving bucket metadata',
                        { error: err });
                    return next(err);
                }
                // check whether bucket has transient or deleted flag
                if (bucketShield(bucketMD, 'objectDelete')) {
                    return next(errors.NoSuchBucket);
                }
                // The implicit deny flag is ignored in the DeleteObjects API, as authorization only
                // affects the objects.
                if (!isBucketAuthorized(bucketMD, 'objectDelete', canonicalID, authInfo, log, request)) {
                    log.trace("access denied due to bucket acl's");
                    // if access denied at the bucket level, no access for
                    // any of the objects so all results will be error results
                    objects.forEach(entry => {
                        errorResults.push({
                            entry,
                            error: errors.AccessDenied,
                        });
                    });
                    // by sending an empty array as the objects array
                    // async.forEachLimit below will not actually
                    // make any calls to metadata or data but will continue on
                    // to the next step to build xml
                    return next(null, quietSetting, errorResults, [], bucketMD);
                }
                return next(null, quietSetting, errorResults, objects, bucketMD);
            });
        },
        function checkPolicies(quietSetting, errorResults, objects, bucketMD, next) {
            // track keys that are still on track to be deleted
            const inPlay = [];
            // if request from account, no need to check policies
            // all objects are inPlay so send array of object keys
            // as inPlay argument
            if (!isRequesterNonAccountUser(authInfo)) {
                return next(null, quietSetting, errorResults, objects, bucketMD);
            }

            // TODO: once arsenal's extractParams is separated from doAuth
            // function, refactor so only extract once and send
            // params on to this api
            const authParams = auth.server.extractParams(request, log,
                's3', request.query);
            const requestContextParams = {
                constantParams: {
                    headers: request.headers,
                    query: request.query,
                    generalResource: request.bucketName,
                    requesterIp: ip,
                    sslEnabled: isSecure,
                    apiMethod: 'objectDelete',
                    awsService: 's3',
                    locationConstraint: null,
                    requesterInfo: authInfo,
                    signatureVersion: authParams.params.data.authType,
                    authType: authParams.params.data.signatureVersion,
                    signatureAge: authParams.params.data.signatureAge,
                },
                parameterize: {
                    // eslint-disable-next-line
                    specificResource: objects.map(entry => {
                        return {
                            key: entry.key,
                            versionId: entry.versionId,
                        };
                    }),
                },
            };
            return vault.checkPolicies(requestContextParams, authInfo.getArn(),
                log, (err, authorizationResults) => {
                    // there were no policies so received a blanket AccessDenied
                    if (err?.is?.AccessDenied) {
                        objects.forEach(entry => {
                            errorResults.push({
                                entry,
                                error: errors.AccessDenied });
                        });
                        // send empty array for inPlay
                        return next(null, quietSetting, errorResults, [], bucketMD);
                    }
                    if (err) {
                        log.trace('error checking policies', {
                            error: err,
                            method: 'multiObjectDelete.checkPolicies',
                        });
                        return next(err);
                    }
                    if (objects.length !== authorizationResults.length) {
                        log.error('vault did not return correct number of ' +
                        'authorization results', {
                            authorizationResultsLength:
                                authorizationResults.length,
                            objectsLength: objects.length,
                        });
                        return next(errors.InternalError);
                    }
                     // Convert authorization results into an easier to handle format
                    const actionImplicitDenies = authorizationResults.reduce((acc, curr, idx) => {
                        const apiMethod = authorizationResults[idx].action;
                        // eslint-disable-next-line no-param-reassign
                        acc[apiMethod] = curr.isImplicit;
                        return acc;
                    }, {});
                    for (let i = 0; i < authorizationResults.length; i++) {
                        const result = authorizationResults[i];
                        // result is { isAllowed: true,
                        // arn: arn:aws:s3:::bucket/object,
                        // versionId: sampleversionId } unless not allowed
                        // in which case no isAllowed key will be present
                        const slashIndex = result.arn.indexOf('/');
                        if (slashIndex === -1) {
                            log.error('wrong arn format from vault');
                            return next(errors.InternalError);
                        }
                        const entry = {
                            key: result.arn.slice(slashIndex + 1),
                            versionId: result.versionId,
                        };
                        // Deny immediately if there is an explicit deny
                        if (!result.isImplicit && !result.isAllowed) {
                            errorResults.push({
                                entry,
                                error: errors.AccessDenied,
                            });
                            continue;
                        }

                        // Evaluate against the bucket policies
                        const areAllActionsAllowed = evaluateBucketPolicyWithIAM(
                            bucketMD,
                            Object.keys(actionImplicitDenies),
                            canonicalID,
                            authInfo,
                            actionImplicitDenies,
                            log,
                            request);

                        if (areAllActionsAllowed) {
                            if (validObjectKeys.includes(entry.key)) {
                                inPlayInternal.push(entry.key);
                            } else {
                                inPlay.push(entry);
                            }
                        } else {
                            errorResults.push({
                                entry,
                                error: errors.AccessDenied,
                            });
                        }
                    }
                    return next(null, quietSetting, errorResults, inPlay, bucketMD);
                });
        },
        function handleInternalFiles(quietSetting, errorResults, inPlay, bucketMD, next) {
            return async.each(inPlayInternal,
                (localInPlay, next) => deleteVeeamCapabilities(bucketName, localInPlay, bucketMD, log, next),
                err => next(err, quietSetting, errorResults, inPlay, bucketMD));
        },
        function getObjMetadataAndDeleteStep(quietSetting, errorResults, inPlay,
            bucket, next) {
            return getObjMetadataAndDelete(authInfo, canonicalID, request,
                    bucketName, bucket, quietSetting, errorResults, inPlay,
                    log, next);
        },
    ], (err, quietSetting, errorResults, numOfObjectsRemoved,
        successfullyDeleted, totalContentLengthDeleted, bucket) => {
        const corsHeaders = collectCorsHeaders(request.headers.origin,
            request.method, bucket);
        if (err) {
            monitoring.promMetrics('DELETE', bucketName, err.code,
                'multiObjectDelete');
            return callback(err, null, corsHeaders);
        }
        const xml = _formatXML(quietSetting, errorResults,
            successfullyDeleted);
        const deletedKeys = successfullyDeleted.map(item => item.key);
        const removedDeleteMarkers = successfullyDeleted
            .filter(item => item.isDeleteMarker && item.entry && item.entry.versionId)
            .length;
        pushMetric('multiObjectDelete', log, {
            authInfo,
            canonicalID: bucket ? bucket.getOwner() : '',
            bucket: bucketName,
            keys: deletedKeys,
            byteLength: Number.parseInt(totalContentLengthDeleted, 10),
            numberOfObjects: numOfObjectsRemoved,
            removedDeleteMarkers,
            isDelete: true,
        });
        monitoring.promMetrics('DELETE', bucketName, '200',
        'multiObjectDelete',
        Number.parseInt(totalContentLengthDeleted, 10), null, null,
        numOfObjectsRemoved);
        return callback(null, xml, corsHeaders);
    });
}
multiObjectDelete - Delete multiple objects
@param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
@param {object} request - http.IncomingMessage as modified by
lib/utils and routes/routePOST.js
@param {object} request.headers - request headers
@param {object} request.query - query from request
@param {string} request.post - concatenation of request body
@param {string} request.bucketName - parsed bucketName
@param {string} request.socket.remoteAddress - requester IP
@param {object} log - Werelogs logger
@param {function} callback - callback to server
@return {undefined}