1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

[Python] MS-Graph returns 404 and 412 when uploading attachments concurrently

Discussão em 'Python' iniciado por Stack, Setembro 12, 2025.

  1. Stack

    Stack Membro Participativo

    I have the following python script which is sending emails through MS-Graph using (partly) msgraph-sdk in python. The problem I see is that if I try to upload multiple attachments of the email concurrently I get a lot of 404 and 412 errors from ms-graph when trying to upload the attachment chunks. But if I set the semaphore to 1 (which effectively uploads the attachments sequentially) then I get no errors. Is there anything wrong with my script? Does ms-graph support concurrent attachment uploading?

    Here is my python code


    import asyncio
    import aiohttp

    from mylib import EmailSender
    from mylib import (
    EmailDefinition,
    EmailContent,
    EncodedAttachment,
    )
    from azure.identity import ClientSecretCredential
    from msgraph.graph_service_client import GraphServiceClient
    from mylib import get_logger
    from msgraph.generated.models.message import Message
    from msgraph.generated.models.item_body import ItemBody
    from msgraph.generated.models.body_type import BodyType
    from msgraph.generated.models.recipient import Recipient
    from msgraph.generated.models.email_address import EmailAddress
    from msgraph.generated.models.attachment_type import AttachmentType
    from msgraph.generated.models.attachment_item import AttachmentItem
    from msgraph.generated.users.item.messages.item.attachments.create_upload_session.create_upload_session_post_request_body import (
    CreateUploadSessionPostRequestBody,
    )

    logger = get_logger(__name__)

    CHUNK_SIZE = 3 * 1024 * 1024 # 3MB
    CHUNK_UPLOAD_TIMEOUT_SECONDS = 20


    class EmailSenderMSG(EmailSender):
    def __init__(self, cfg):
    _credential = ClientSecretCredential(
    tenant_id=cfg.tenant_id,
    client_id=cfg.client_id,
    client_secret=cfg.client_secret.get_secret_value(),
    )
    self._graph_client = GraphServiceClient(
    credentials=_credential, scopes=[cfg.scope]
    )

    async def _send_large_email(self, email_def: EmailDefinition):
    # Implementation reference https://learn.microsoft.com/en-us/graph/outlook-large-attachments?tabs=python
    # Step 1: Create Draft
    message = self.create_base_message(email_def)

    draft = self._graph_client.users.by_user_id(email_def.sender_address).messages.post(message)

    # Step 2: Upload attachments concurrently with semaphore (called attachment_upload_concurrency)
    sem = asyncio.Semaphore(4)

    async def upload_one(att: EncodedAttachment) -> None:
    async with sem:
    await self.upload_attachment(email_def.sender_address, draft.id, att)

    tasks = [
    asyncio.create_task(upload_one(att)) for att in email_def.attachments
    ]
    await asyncio.gather(*tasks) # No return_exceptions

    # Step 4: Send the draft
    logger.info(f"Finalising draft message with id {draft.id}")
    await (self._graph_client.users.by_user_id(email_def.sender_address)
    .messages.by_message_id(draft.id).send.post())

    async def upload_attachment(
    self, sender_address: str, draft_message_id: str, attachment: EncodedAttachment) -> None:
    attachment_size = len(attachment.content)
    upload_session_body = CreateUploadSessionPostRequestBody(
    attachment_item=AttachmentItem(
    attachment_type=AttachmentType.File,
    name=attachment.name,
    size=attachment_size,
    content_type=attachment.content_type,
    )
    )

    logger.info(
    f"Creating upload session for attachment '{attachment.name}' of size {attachment_size} bytes. Request body: {upload_session_body}"
    )
    upload_session = await (
    self._graph_client.users.by_user_id(sender_address).messages.by_message_id(draft_message_id)
    .attachments.create_upload_session.post(upload_session_body))
    logger.debug(f"Upload session for attachment '{attachment.name}' : {upload_session}")

    # Step 3: Upload in chunks (simple, no retries for now)
    await self.upload_chunks(attachment, attachment_size, upload_session)

    @staticmethod
    async def upload_chunks(attachment, attachment_size, upload_session):
    async with aiohttp.ClientSession() as session:
    for i in range(0, attachment_size, CHUNK_SIZE):
    chunk = attachment.content[i: i + CHUNK_SIZE]
    start = i
    end = i + len(chunk) - 1
    total = attachment_size

    # msgraph-dsk is not supporting attachment chunk uploading, so we use aiohttp directly
    headers = {
    "Content-Length": str(len(chunk)),
    "Content-Range": f"bytes {start}-{end}/{total}",
    "Content-Type": "application/octet-stream",
    }

    logger.debug(
    f"Uploading chunk of attachment '{attachment.name}' with url {upload_session.upload_url} and headers {headers}"
    )
    await EmailSenderMSG.upload_single_chunk(
    session,
    upload_session.upload_url,
    chunk,
    headers,
    attachment.name,
    )

    @staticmethod
    async def upload_single_chunk(
    session: aiohttp.ClientSession, url: str, chunk: bytes, headers: dict[str, str], attachment_name: str
    ) -> None:
    async with session.put(
    url,
    data=chunk,
    headers=headers,
    timeout=aiohttp.ClientTimeout(total=CHUNK_UPLOAD_TIMEOUT_SECONDS),
    ) as response:
    try:
    response.raise_for_status()
    except aiohttp.ClientResponseError as e:
    logger.warning(
    f"ClientResponseError: {e.status}, attachment '{attachment_name}', {e.message}, Headers: {e.headers}",
    exc_info=True,
    )
    raise

    @staticmethod
    def _create_item_body(email_content: EmailContent) -> ItemBody:
    if email_content.html:
    return ItemBody(content_type=BodyType.Html, content=email_content.html)
    elif email_content.plain_text:
    return ItemBody(
    content_type=BodyType.Text, content=email_content.plain_text
    )

    raise ValueError("Email content must have either 'html' or 'plain_text' set.")

    @staticmethod
    def create_base_message(email_def):
    message = Message(
    subject=email_def.content.subject,
    body=EmailSenderMSG._create_item_body(email_def.content),
    to_recipients=[
    Recipient(email_address=EmailAddress(address=addr))
    for addr in email_def.recipients.to
    ],
    cc_recipients=[
    Recipient(email_address=EmailAddress(address=addr))
    for addr in email_def.recipients.cc
    ],
    bcc_recipients=[
    Recipient(email_address=EmailAddress(address=addr))
    for addr in email_def.recipients.bcc
    ],
    )
    return message

    Continue reading...

Compartilhe esta Página