@future method makes a callout without callout=true

ID

apex.correctness.future_callout_annotation

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Api Usage

Language

Apex

Description

Reports a @future method whose body builds an HttpRequest or sends one, while the annotation does not carry callout=true.

Rationale

The platform decides whether an asynchronous method may open a connection from the annotation alone, before the method runs. Without callout=true the send throws CalloutException: Callout from scheduled Apex not supported - and because the method runs asynchronously, nothing surfaces at the call site: the enqueuing transaction commits happily and the failure lands in the Apex Jobs list, minutes later, for whoever thinks to look. The same code called synchronously works, so the mistake usually ships from a refactor that moved a working callout into a @future.

The following code illustrates the pattern detected by this rule:

public class BillingSync {

    // FLAGGED: @future method makes a callout without callout=true
    @future
    public static void pushInvoice(Set<Id> invoiceIds) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Billing/invoices');
        req.setMethod('POST');
        Http http = new Http();
        HttpResponse res = http.send(req);
        System.debug(res.getStatusCode());
    }

    // FLAGGED: @future method makes a callout without callout=true
    @future(timeout=60000)
    public static void pushCredit(Set<Id> creditIds) {
        Http http = new Http();
        http.send(new HttpRequest());
    }

    @future(callout=true)
    public static void pushRefund(Set<Id> refundIds) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Billing/refunds');
        Http http = new Http();
        http.send(req);
    }

    @future
    public static void recalculate(Set<Id> invoiceIds) {
        List<Invoice__c> invoices = [SELECT Id FROM Invoice__c];
        update invoices;
    }
}

Remediation

Write @future(callout=true). Where the method also needs to be called from a batch or a Queueable, move the work into a Queueable that implements Database.AllowsCallouts instead - @future cannot be chained and cannot be called from another asynchronous context.

Configuration

This detector does not need any configuration.

© 2026 Xygeni. All rights reserved