Skip to content

Bug Report: Binance API Signature Works with Requests but Fails with Aiohttp #11447

@river-walras

Description

@river-walras

Describe the bug

Summary

The Binance API signature authentication works correctly when using the requests library but fails with "signature not work fine for binance" error when using aiohttp. Both functions use identical signature generation logic, but the HTTP request handling differs between the two libraries.

Environment

  • Python version: 3.11+
  • Libraries: requests, aiohttp, hmac, hashlib
  • API: Binance Futures Testnet (https://testnet.binancefuture.com)

Expected Behavior

Both send_signed_request() (using requests) and async_send_signed_request() (using aiohttp) should successfully authenticate with Binance API using the same signature generation method.

Code Comparison

Working Version (Requests)

def send_signed_request(http_method, url_path, payload={}):
    query_string = urlencode(payload)
    query_string = query_string.replace("%27", "%22")
    if query_string:
        query_string = "{}&timestamp={}".format(query_string, get_timestamp())
    else:
        query_string = "timestamp={}".format(get_timestamp())

    url = (
        BASE_URL + url_path + "?" + query_string + "&signature=" + hashing(query_string)
    )
    params = {"url": url, "params": {}}
    response = dispatch_request(http_method)(**params)
    return response.json()

Failing Version (Aiohttp)

async def async_send_signed_request(http_method, url_path, payload={}):
    query_string = urlencode(payload)
    query_string = query_string.replace("%27", "%22")
    if query_string:
        query_string = "{}&timestamp={}".format(query_string, get_timestamp())
    else:
        query_string = "timestamp={}".format(get_timestamp())

    url = (
        BASE_URL + url_path + "?" + query_string + "&signature=" + hashing(query_string)
    )
    params = {"url": url, "params": {}}

    async with aiohttp.ClientSession(headers={"Content-Type": "application/json;charset=utf-8", "X-MBX-APIKEY": KEY}) as session:
        async with session.request(http_method, **params) as response:
            return await response.json()

Actual Behavior

  • send_signed_request() with requests: Works correctly
  • async_send_signed_request() with aiohttp: Returns signature error from Binance

To Reproduce

Here is the full code:

import hmac
import time
import hashlib
import requests
import aiohttp
import asyncio
from urllib.parse import urlencode


KEY = ""
SECRET = ""
BASE_URL = "https://testnet.binancefuture.com"  # testnet base url

def hashing(query_string):
    return hmac.new(
        SECRET.encode("utf-8"), query_string.encode("utf-8"), hashlib.sha256
    ).hexdigest()


def get_timestamp():
    return int(time.time() * 1000)


def dispatch_request(http_method):
    session = requests.Session()
    session.headers.update(
        {"Content-Type": "application/json;charset=utf-8", "X-MBX-APIKEY": KEY}
    )
    return {
        "GET": session.get,
        "DELETE": session.delete,
        "PUT": session.put,
        "POST": session.post,
    }.get(http_method, "GET")


# used for sending request requires the signature
def send_signed_request(http_method, url_path, payload={}):
    query_string = urlencode(payload)
    # replace single quote to double quote
    query_string = query_string.replace("%27", "%22")
    if query_string:
        query_string = "{}&timestamp={}".format(query_string, get_timestamp())
    else:
        query_string = "timestamp={}".format(get_timestamp())

    url = (
        BASE_URL + url_path + "?" + query_string + "&signature=" + hashing(query_string)
    )
    print("{} {}".format(http_method, url))
    params = {"url": url, "params": {}}
    response = dispatch_request(http_method)(**params)
    return response.json()

async def async_send_signed_request(http_method, url_path, payload={}):
    query_string = urlencode(payload)
    # replace single quote to double quote
    query_string = query_string.replace("%27", "%22")
    if query_string:
        query_string = "{}&timestamp={}".format(query_string, get_timestamp())
    else:
        query_string = "timestamp={}".format(get_timestamp())

    url = (
        BASE_URL + url_path + "?" + query_string + "&signature=" + hashing(query_string)
    )
    print("{} {}".format(http_method, url))
    params = {"url": url, "params": {}}

    async with aiohttp.ClientSession(headers={"Content-Type": "application/json;charset=utf-8", "X-MBX-APIKEY": KEY}) as session:
        async with session.request(http_method, **params) as response:
            return await response.json()


# used for sending public data request
def send_public_request(url_path, payload={}):
    query_string = urlencode(payload, True)
    url = BASE_URL + url_path
    if query_string:
        url = url + "?" + query_string
    print("{}".format(url))
    response = dispatch_request("GET")(url=url)
    return response.json()

params = {
    "batchOrders": [
        {
            "symbol": "BTCUSDT",
            "side": "BUY",
            "quantity": "0.01",
            "type": "LIMIT",
            "price": "110575.1",
            "timeInForce": "GTC",
        },
        {
            "symbol": "BTCUSDT",
            "side": "BUY",
            "quantity": "0.01",
            "type": "LIMIT",
            "price": "110464.0",
            "timeInForce": "GTC",
        },
    ]
}
response = send_signed_request("POST", "/fapi/v1/batchOrders", params)
print(response)

async def main():
    response = await async_send_signed_request("POST", "/fapi/v1/batchOrders", params)
    print(response)

if __name__ == "__main__":
    asyncio.run(main())

You can get the apiKey and secret from Binance Testnet

Image

Expected behavior

You would get

## requests 
POST https://testnet.binancefuture.com/fapi/v1/batchOrders?batchOrders=%5B%7B%22symbol%22%3A+%22BTCUSDT%22%2C+%22side%22%3A+%22BUY%22%2C+%22quantity%22%3A+%220.01%22%2C+%22type%22%3A+%22LIMIT%22%2C+%22price%22%3A+%22110575.1%22%2C+%22timeInForce%22%3A+%22GTC%22%7D%2C+%7B%22symbol%22%3A+%22BTCUSDT%22%2C+%22side%22%3A+%22BUY%22%2C+%22quantity%22%3A+%220.01%22%2C+%22type%22%3A+%22LIMIT%22%2C+%22price%22%3A+%22110464.0%22%2C+%22timeInForce%22%3A+%22GTC%22%7D%5D&timestamp=1756127492040&signature=160e01e70194b4f48f9f4c4c7b933c7755cc82aef08513eb6c6e974a4850c932
[{'orderId': 5609573324, 'symbol': 'BTCUSDT', 'status': 'NEW', 'clientOrderId': 'i2SDQszYoiIQ3Uw2hJFqAg', 'price': '110575.10', 'avgPrice': '0.00', 'origQty': '0.010', 'executedQty': '0.000', 'cumQty': '0.000', 'cumQuote': '0.00000', 'timeInForce': 'GTC', 'type': 'LIMIT', 'reduceOnly': False, 'closePosition': False, 'side': 'BUY', 'positionSide': 'BOTH', 'stopPrice': '0.00', 'workingType': 'CONTRACT_PRICE', 'priceProtect': False, 'origType': 'LIMIT', 'priceMatch': 'NONE', 'selfTradePreventionMode': 'EXPIRE_MAKER', 'goodTillDate': 0, 'updateTime': 1756127492237}, {'orderId': 5609573323, 'symbol': 'BTCUSDT', 'status': 'NEW', 'clientOrderId': 'KWt009fJyFVrvBu8Y4xD7x', 'price': '110464.00', 'avgPrice': '0.00', 'origQty': '0.010', 'executedQty': '0.000', 'cumQty': '0.000', 'cumQuote': '0.00000', 'timeInForce': 'GTC', 'type': 'LIMIT', 'reduceOnly': False, 'closePosition': False, 'side': 'BUY', 'positionSide': 'BOTH', 'stopPrice': '0.00', 'workingType': 'CONTRACT_PRICE', 'priceProtect': False, 'origType': 'LIMIT', 'priceMatch': 'NONE', 'selfTradePreventionMode': 'EXPIRE_MAKER', 'goodTillDate': 0, 'updateTime': 1756127492237}]

## aiohttp 
POST https://testnet.binancefuture.com/fapi/v1/batchOrders?batchOrders=%5B%7B%22symbol%22%3A+%22BTCUSDT%22%2C+%22side%22%3A+%22BUY%22%2C+%22quantity%22%3A+%220.01%22%2C+%22type%22%3A+%22LIMIT%22%2C+%22price%22%3A+%22110575.1%22%2C+%22timeInForce%22%3A+%22GTC%22%7D%2C+%7B%22symbol%22%3A+%22BTCUSDT%22%2C+%22side%22%3A+%22BUY%22%2C+%22quantity%22%3A+%220.01%22%2C+%22type%22%3A+%22LIMIT%22%2C+%22price%22%3A+%22110464.0%22%2C+%22timeInForce%22%3A+%22GTC%22%7D%5D&timestamp=1756127492279&signature=daf8a1138f7582a8d9568edca1e222978c7bb065fe717c06e16d0a512b583a6d
{'code': -1022, 'msg': 'Signature for this request is not valid.'}

The results should be the same

Logs/tracebacks

None

Python Version

$ python --version
Python 3.12.3

aiohttp Version

$ python -m pip show aiohttp
Name: aiohttp
Version: 3.12.15
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Author: 
Author-email: 
License: Apache-2.0 AND MIT
Location: /root/NexusTrader/.venv/lib/python3.12/site-packages
Requires: aiohappyeyeballs, aiosignal, attrs, frozenlist, multidict, propcache, yarl
Required-by: ccxt, nexustrader

multidict Version

$ python -m pip show multidict
Name: multidict
Version: 6.2.0
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /root/NexusTrader/.venv/lib/python3.12/site-packages
Requires: 
Required-by: aiohttp, picows, yarl

propcache Version

$ python -m pip show propcache
Name: propcache
Version: 0.3.1
Summary: Accelerated property cache
Home-page: https://github.com/aio-libs/propcache
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: /root/NexusTrader/.venv/lib/python3.12/site-packages
Requires: 
Required-by: aiohttp, yarl

yarl Version

$ python -m pip show yarl
Name: yarl
Version: 1.20.0
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: /root/NexusTrader/.venv/lib/python3.12/site-packages
Requires: idna, multidict, propcache
Required-by: aiohttp, ccxt

OS

Linux Ubuntu

Related component

Client

Additional context

No response

Code of Conduct

  • I agree to follow the aio-libs Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-infoIssue is lacking sufficient information and will be closed if not provided

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions