HTTP outcall inconsistency

I’m developing a canister on the IC that includes a sendNotification function. The function works as expected when called directly, but when called from within another function, it returns a bad request . Additionally, I noticed that if I pass the arguments to sendNotification directly from the parent function’s parameters, it works fine. However, if I pass the arguments from variables defined within the parent function, it fails.

Details

  • Functionality:
    • The sendNotification function sends an email notification using an external service.
    • It takes three parameters: to (Text), subject (Text), and body (Text).
  • Behavior:
    • Direct Call: Calling sendNotification("user@dfinity.com", "Hello", "Test") works correctly.
    • Indirect Call: Calling sendNotification from another function with variables (let to = "user@dfinity.com"; let subject = "Hello"; let body = "Test";) results in a bad request.
    • Observation: If I parse the arguments directly from the parent function’s parameters, it works. But if I pass the arguments from variables within the function, it fails.

What I’ve Tried

  1. Type Checking: Ensuring that the variables are of type Text and not some other type.
  2. Simplifying the Code: Removing unnecessary logic and testing with hardcoded values.

Anyone with insight to this?

In both cases, it should work the same. It’s hard to conclude anything, but if you can add your code, it would be easier.
Other than this, the only 1 thing that changes when you call a function directly and when you call a function from within another function, is the ({caller}) of the function.
When you call sendNotification function directly, {caller} of the function is the actual client or user who is invoking it, but in the second case, {caller} for sendNotification function would be the canister itself and not the client/user who invoked the parent function.

2 Likes

Thank you for your response! I appreciate the insight about the caller context changing between direct and indirect function calls. However, after further investigation, I think that the issue might be related to JSON serialization when concatenating variables into the HTTP request body.

Here’s a summary of my findings:

  1. Problem:
  • When I call sendNotification directly with hardcoded values, it works fine.
  • When I call it from another function (e.g., confirmDeposit) with variables or concatenated arguments, it returns a bad request.
  1. Possible Root Cause:
  • The issue is probably because the JSON string constructed for the HTTP request body becomes malformed when variables are concatenated.
  • Special characters (e.g., ", \, \n) in the input variables are not properly escaped, causing the JSON to break.

I will try to work on solutions and update it here.

Just for reference this is the code

public func sendNotification(subject : Text, body : Text, receiver_email : Text) : async () {
    let ic : HTTP.IC = actor ("aaaaa-aa");

    let requestBodyJson : Text = "{ \"to\": \"" # receiver_email # "\", \"subject\": \"" # subject # "\", \"body\": \"" # body # "\"}";
    let requestBodyAsBlob : Blob = Text.encodeUtf8(requestBodyJson);
    let requestBodyAsNat8 : [Nat8] = Blob.toArray(requestBodyAsBlob);

    let transform_context : HTTP.TransformContext = {
        function = transform;
        context = Blob.fromArray([])
    };

    // Setup request
    let httpRequest : HTTP.HttpRequestArgs = {
      url = "https://notifier.onrender.com/send-email";
      max_response_bytes = ?Nat64.fromNat(1000);
      headers = [
        { name = "Content-Type"; value = "application/json" },
      ];
      body = ?requestBodyAsNat8;
      method = #post;
      transform = ?transform_context;
    };
    
    Cycles.add(80_000_000);

    // Send the request
    let httpResponse : HTTP.HttpResponsePayload = await ic.http_request(httpRequest);

    // Check the response
    if (httpResponse.status > 299) {
      let response_body : Blob = Blob.fromArray(httpResponse.body);
      let decoded_text : Text = switch (Text.decodeUtf8(response_body)) {
        case (null) { "No value returned" };
        case (?y) { y };
      };
      throw Error.reject("Error sending notification: " # decoded_text);
    } else {
      Debug.print("Notification sent");
    };
};

public shared ({ caller }) func confirmDeposit(amount : Nat, time : Text) : async Text {
        await sendNotification(
            "New ICP Deposit Confirmation", 
            "User with Principal: " # Principal.toText(caller) # " has initiated a deposit of " # Nat.toText(amount) # " Naira.\nPlease verify the payment and fund their wallet.\n\nTimestamp: " # time, 
            "finisher@dfinity.com"
        );
        
        return "Deposit confirmation sent to admin. Please await deposit";
    };

Not sure, but I expect the problem is the “\n” in the strings below. They will be embedded in JSON string literals so probably need another level of JSON escaping: “\n” might do it.

1 Like

Thanks for the insight. The problem was indeed with the “\n”. A function like this did the job

func textCleaner(text : Text) : Text {
        let replacements = [
            ("\\", "\\\\"), 
            ("\"", "\\\""), 
            ("\n", "\\n"),  
            ("\t", "\\t"), 
            ("\r", "\\r"),  
        ];

        var result = text;
        for ((search, replace) in replacements.vals()) {
            result := Text.replace(result, #text search, replace);
        };
        return result;
    };

So basically, the arguments are cleaned before used.

1 Like