Thanks to visit codestin.com
Credit goes to github.com

Skip to content

BatchHttpRequest Fails When Mixing files.modifyLabels and files.update Methods #2655

@mfcarroll

Description

@mfcarroll

Environment details

  • OS type and version: macOS 15.6.1
  • Python version: 3.13.7
  • pip version: 25.2
  • google-api-python-client version: 2.181.0

Description

When attempting to batch API calls using service.new_batch_http_request(), the batch fails if it contains both a files().modifyLabels() request and a files().update() request. The overall batch request returns a 200 OK, but the individual responses for each sub-request show a 400 Bad Request.

This issue occurs regardless of whether the operations target the same file ID or different file IDs.

The batching mechanism works as expected in the following scenarios:

  1. When the modifyLabels and update calls are placed in two separate batch requests.
  2. When multiple modifyLabels calls are placed in a single batch request.
  3. When multiple update calls are placed in a single batch request.

There is either an incompatibility between these two methods within the batching feature, or a general problem with batching differing request types.

Note, I did test this out building the requests manually and the behaviour is the same, so it appears to be an underlying bug in the API itself, rather than specific to the python client.

Steps to reproduce

The following script provides a minimal, reproducible example.

Prerequisites:

  1. A Google Cloud Project with the Drive API enabled.
  2. Authenticated Application Default Credentials (gcloud auth application-default login).
  3. Two Google Docs with their file IDs.
  4. A Google Drive Label created with a Selection field.
  • DRIVE_LABEL_ID: The ID of the label itself.
  • LABEL_FIELD_ID: The ID of the selection field within the label.
  • FIELD_CHOICE_ID: The ID of one of the choices for that selection field.

Code example

import json
from datetime import datetime

import google.auth
import httplib2
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

httplib2.debuglevel = 4

# --- Configuration (NEEDS TO BE SET BY USER) ---
# To reproduce, create two Google Docs and a Drive Label with a selection field.
FILE1_ID = "17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg"
FILE2_ID = "1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM"
DRIVE_LABEL_ID = "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb"
LABEL_FIELD_ID = "789D8ADF26"
FIELD_CHOICE_ID = "844694D02D"


def batch_callback(request_id: str, response: dict, exception: HttpError):
    """Callback function to print the result of each API call."""
    if exception:
        print(f"\n[FAILED] Request ID: {request_id}\nException: {exception}\n")
    else:
        print(f"\n[SUCCESS] Request ID: {request_id}\n")
        print(f"Response: {json.dumps(response, indent=2)}")


def run_batch_request(drive_service, requests: list, description: str):
    """Helper to build and execute a batch request."""
    print(f"\n--- {description} ---\n")
    try:
        batch = drive_service.new_batch_http_request(callback=batch_callback)
        for req in requests:
            batch.add(req)
        batch.execute()
    except Exception as e:
        print(f"An unexpected error occurred: {e}")


def main():
    """Demonstrates the Google Drive API batching bug with detailed logging."""
    credentials, _ = google.auth.default(
        scopes=["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.labels"]
    )
    credentials.refresh(Request())
    drive_service = build("drive", "v3", credentials=credentials)

    # Define the request bodies, which are proven to work in single calls
    modify_labels_body = {
        "kind": "drive#modifyLabelsRequest",
        "labelModifications": [
            {
                "kind": "drive#labelModification",
                "labelId": DRIVE_LABEL_ID,
                "fieldModifications": [{"fieldId": LABEL_FIELD_ID, "setSelectionValues": [FIELD_CHOICE_ID]}],
            }
        ],
    }
    update_body = {"properties": {"testSource": f"batch-bug-report-{datetime.now().isoformat()}"}}

    # --- Test Case 1: Separate Batches (Success) ---
    run_batch_request(
        drive_service,
        [drive_service.files().modifyLabels(fileId=FILE1_ID, body=modify_labels_body)],
        "Test Case 1a: Separate Batch - Single modifyLabels Request",
    )

    run_batch_request(
        drive_service,
        [drive_service.files().update(fileId=FILE2_ID, body=update_body, supportsAllDrives=True)],
        "Test Case 1b: Separate Batch - Single update Request",
    )

    # --- Test Case 2: Batch of Same Method (Success) ---
    run_batch_request(
        drive_service,
        [
            drive_service.files().modifyLabels(fileId=FILE1_ID, body=modify_labels_body),
            drive_service.files().modifyLabels(fileId=FILE2_ID, body=modify_labels_body),
        ],
        "Test Case 2a: Single Batch - Two modifyLabels Requests",
    )

    run_batch_request(
        drive_service,
        [
            drive_service.files().update(fileId=FILE1_ID, body=update_body, supportsAllDrives=True),
            drive_service.files().update(fileId=FILE2_ID, body=update_body, supportsAllDrives=True),
        ],
        "Test Case 2b: Single Batch - Two update Requests",
    )

    # --- Test Case 3: Mixed Batch (Failure) ---
    run_batch_request(
        drive_service,
        [
            drive_service.files().modifyLabels(fileId=FILE1_ID, body=modify_labels_body),
            drive_service.files().update(fileId=FILE2_ID, body=update_body, supportsAllDrives=True),
        ],
        "Test Case 3: Single Batch - Mixed Batch (Failure)",
    )


if __name__ == "__main__":
    main()

Expected Result:

  • All three test cases in the script should execute successfully, with each API call within the batches returning a 200 OK status in its individual response.

Actual Result:

  • Test Case 1 (Separate Batches) succeeds.
  • Test Case 2 (Batch of Same Method) succeeds.
  • Test Case 3 (Mixed Batch) fails. The batch request returns 200 OK, but the internal responses for both the modifyLabels and update calls are 400 Bad Request.

Stack trace

--- Test Case 1a: Separate Batch - Single modifyLabels Request ---

connect: (www.googleapis.com, 443)
send: b'POST /batch/drive/v3 HTTP/1.1\r\nHost: www.googleapis.com\r\nContent-Length: 1101\r\ncontent-type: multipart/mixed; boundary="===============6087181464682891008=="\r\nx-goog-api-client: cred-type/u\r\nauthorization: Bearer ***\r\nx-goog-user-project: ***\r\nuser-agent: Python-httplib2/0.30.0 (gzip)\r\naccept-encoding: gzip, deflate\r\n\r\n'
send: b'--===============6087181464682891008==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <42ec34b7-21cc-4347-9e4e-b22bccea35ac + 1>\n\nPOST /drive/v3/files/17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg/modifyLabels?alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 246\n\n{"kind": "drive#modifyLabelsRequest", "labelModifications": [{"kind": "drive#labelModification", "labelId": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb", "fieldModifications": [{"fieldId": "789D8ADF26", "setSelectionValues": ["844694D02D"]}]}]}\n--===============6087181464682891008==--\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: content-type: multipart/mixed; boundary=batch_d5mL9UOa4ZIp58SsbGt0qiTTMT1BR9oI
header: Expires: Mon, 01 Jan 1990 00:00:00 GMT
header: Pragma: no-cache
header: Date: Thu, 11 Sep 2025 01:08:04 GMT
header: Vary: Origin, X-Origin
header: Cache-Control: no-cache, no-store, max-age=0, must-revalidate
header: Content-Encoding: gzip
header: Server: ESF
header: X-XSS-Protection: 0
header: X-Frame-Options: SAMEORIGIN
header: X-Content-Type-Options: nosniff
header: Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
header: Transfer-Encoding: chunked

[SUCCESS] Request ID: 1

Response: {
  "modifiedLabels": [
    {
      "fields": {
        "789D8ADF26": {
          "selection": [
            "844694D02D"
          ],
          "kind": "drive#labelField",
          "id": "789D8ADF26",
          "valueType": "selection"
        }
      },
      "kind": "drive#label",
      "id": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb",
      "revisionId": "59"
    }
  ],
  "kind": "drive#modifyLabelsResponse"
}

--- Test Case 1b: Separate Batch - Single update Request ---

send: b'POST /batch/drive/v3 HTTP/1.1\r\nHost: www.googleapis.com\r\nContent-Length: 942\r\ncontent-type: multipart/mixed; boundary="===============0571320967349974227=="\r\nx-goog-api-client: cred-type/u\r\nauthorization: Bearer ***\r\nx-goog-user-project: ***\r\nuser-agent: Python-httplib2/0.30.0 (gzip)\r\naccept-encoding: gzip, deflate\r\n\r\n'
send: b'--===============0571320967349974227==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <4ac4afcf-d99e-4c78-884d-f22dd0b637c8 + 1>\n\nPATCH /drive/v3/files/1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM?supportsAllDrives=true&alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 77\n\n{"properties": {"testSource": "batch-bug-report-2025-09-10T18:08:03.770234"}}\n--===============0571320967349974227==--\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: content-type: multipart/mixed; boundary=batch_1tdnt60ZX3EQHMkXBBIoTxsn7jV4TN1G
header: Pragma: no-cache
header: Date: Thu, 11 Sep 2025 01:08:04 GMT
header: Cache-Control: no-cache, no-store, max-age=0, must-revalidate
header: Expires: Mon, 01 Jan 1990 00:00:00 GMT
header: Vary: Origin, X-Origin
header: Content-Encoding: gzip
header: Server: ESF
header: X-XSS-Protection: 0
header: X-Frame-Options: SAMEORIGIN
header: X-Content-Type-Options: nosniff
header: Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
header: Transfer-Encoding: chunked

[SUCCESS] Request ID: 1

Response: {
  "kind": "drive#file",
  "id": "1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM",
  "name": "Test doc 2",
  "mimeType": "application/vnd.google-apps.document",
  "teamDriveId": "0AGJpjlgQ1LAkUk9PVA",
  "driveId": "0AGJpjlgQ1LAkUk9PVA"
}

--- Test Case 2a: Single Batch - Two modifyLabels Requests ---

send: b'POST /batch/drive/v3 HTTP/1.1\r\nHost: www.googleapis.com\r\nContent-Length: 2161\r\ncontent-type: multipart/mixed; boundary="===============5731707871754303950=="\r\nx-goog-api-client: cred-type/u\r\nauthorization: Bearer ***\r\nx-goog-user-project: ***\r\nuser-agent: Python-httplib2/0.30.0 (gzip)\r\naccept-encoding: gzip, deflate\r\n\r\n'
send: b'--===============5731707871754303950==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <58b02175-2a77-48b5-abc5-324c9ece6a0b + 1>\n\nPOST /drive/v3/files/17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg/modifyLabels?alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 246\n\n{"kind": "drive#modifyLabelsRequest", "labelModifications": [{"kind": "drive#labelModification", "labelId": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb", "fieldModifications": [{"fieldId": "789D8ADF26", "setSelectionValues": ["844694D02D"]}]}]}\n--===============5731707871754303950==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <58b02175-2a77-48b5-abc5-324c9ece6a0b + 2>\n\nPOST /drive/v3/files/1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM/modifyLabels?alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 246\n\n{"kind": "drive#modifyLabelsRequest", "labelModifications": [{"kind": "drive#labelModification", "labelId": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb", "fieldModifications": [{"fieldId": "789D8ADF26", "setSelectionValues": ["844694D02D"]}]}]}\n--===============5731707871754303950==--\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: content-type: multipart/mixed; boundary=batch_CiOxUm0lV6_9OlQFDlKFj8wZ9uo7aHif
header: Expires: Mon, 01 Jan 1990 00:00:00 GMT
header: Pragma: no-cache
header: Date: Thu, 11 Sep 2025 01:08:05 GMT
header: Cache-Control: no-cache, no-store, max-age=0, must-revalidate
header: Vary: Origin, X-Origin
header: Content-Encoding: gzip
header: Server: ESF
header: X-XSS-Protection: 0
header: X-Frame-Options: SAMEORIGIN
header: X-Content-Type-Options: nosniff
header: Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
header: Transfer-Encoding: chunked

[SUCCESS] Request ID: 1

Response: {
  "modifiedLabels": [
    {
      "fields": {
        "789D8ADF26": {
          "selection": [
            "844694D02D"
          ],
          "kind": "drive#labelField",
          "id": "789D8ADF26",
          "valueType": "selection"
        }
      },
      "kind": "drive#label",
      "id": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb",
      "revisionId": "59"
    }
  ],
  "kind": "drive#modifyLabelsResponse"
}

[SUCCESS] Request ID: 2

Response: {
  "modifiedLabels": [
    {
      "fields": {
        "789D8ADF26": {
          "selection": [
            "844694D02D"
          ],
          "kind": "drive#labelField",
          "id": "789D8ADF26",
          "valueType": "selection"
        }
      },
      "kind": "drive#label",
      "id": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb",
      "revisionId": "59"
    }
  ],
  "kind": "drive#modifyLabelsResponse"
}

--- Test Case 2b: Single Batch - Two update Requests ---

send: b'POST /batch/drive/v3 HTTP/1.1\r\nHost: www.googleapis.com\r\nContent-Length: 1843\r\ncontent-type: multipart/mixed; boundary="===============8192210390717026875=="\r\nx-goog-api-client: cred-type/u\r\nauthorization: Bearer ***\r\nx-goog-user-project: ***\r\nuser-agent: Python-httplib2/0.30.0 (gzip)\r\naccept-encoding: gzip, deflate\r\n\r\n'
send: b'--===============8192210390717026875==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <83471f88-19c1-4a58-ad14-ba712c82e9aa + 1>\n\nPATCH /drive/v3/files/17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg?supportsAllDrives=true&alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 77\n\n{"properties": {"testSource": "batch-bug-report-2025-09-10T18:08:03.770234"}}\n--===============8192210390717026875==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <83471f88-19c1-4a58-ad14-ba712c82e9aa + 2>\n\nPATCH /drive/v3/files/1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM?supportsAllDrives=true&alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 77\n\n{"properties": {"testSource": "batch-bug-report-2025-09-10T18:08:03.770234"}}\n--===============8192210390717026875==--\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: content-type: multipart/mixed; boundary=batch_aHiY_4m6254KP-3UgtdqMZuLO1AdQGvI
header: Cache-Control: no-cache, no-store, max-age=0, must-revalidate
header: Date: Thu, 11 Sep 2025 01:08:06 GMT
header: Vary: Origin, X-Origin
header: Expires: Mon, 01 Jan 1990 00:00:00 GMT
header: Pragma: no-cache
header: Content-Encoding: gzip
header: Server: ESF
header: X-XSS-Protection: 0
header: X-Frame-Options: SAMEORIGIN
header: X-Content-Type-Options: nosniff
header: Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
header: Transfer-Encoding: chunked

[SUCCESS] Request ID: 1

Response: {
  "kind": "drive#file",
  "id": "17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg",
  "name": "Test doc",
  "mimeType": "application/vnd.google-apps.document",
  "teamDriveId": "0AGJpjlgQ1LAkUk9PVA",
  "driveId": "0AGJpjlgQ1LAkUk9PVA"
}

[SUCCESS] Request ID: 2

Response: {
  "kind": "drive#file",
  "id": "1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM",
  "name": "Test doc 2",
  "mimeType": "application/vnd.google-apps.document",
  "teamDriveId": "0AGJpjlgQ1LAkUk9PVA",
  "driveId": "0AGJpjlgQ1LAkUk9PVA"
}

--- Test Case 3: Single Batch - Mixed Batch (Failure) ---

send: b'POST /batch/drive/v3 HTTP/1.1\r\nHost: www.googleapis.com\r\nContent-Length: 2002\r\ncontent-type: multipart/mixed; boundary="===============6157900518903741992=="\r\nx-goog-api-client: cred-type/u\r\nauthorization: Bearer ***\r\nx-goog-user-project: ***\r\nuser-agent: Python-httplib2/0.30.0 (gzip)\r\naccept-encoding: gzip, deflate\r\n\r\n'
send: b'--===============6157900518903741992==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <18367dd2-d452-4e28-b363-a6c76704bfaa + 1>\n\nPOST /drive/v3/files/17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg/modifyLabels?alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 246\n\n{"kind": "drive#modifyLabelsRequest", "labelModifications": [{"kind": "drive#labelModification", "labelId": "Ox6e6sJsPu2BHkC0Q6flhbI8sSx7Di7om27RNNEbbFcb", "fieldModifications": [{"fieldId": "789D8ADF26", "setSelectionValues": ["844694D02D"]}]}]}\n--===============6157900518903741992==\nContent-Type: application/http\nMIME-Version: 1.0\nContent-Transfer-Encoding: binary\nContent-ID: <18367dd2-d452-4e28-b363-a6c76704bfaa + 2>\n\nPATCH /drive/v3/files/1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM?supportsAllDrives=true&alt=json HTTP/1.1\nContent-Type: application/json\nMIME-Version: 1.0\naccept: application/json\naccept-encoding: gzip, deflate\nuser-agent: (gzip)\nx-goog-api-client: gdcl/2.181.0 gl-python/3.13.7\nauthorization: Bearer ***\nx-goog-user-project: ***\nHost: www.googleapis.com\ncontent-length: 77\n\n{"properties": {"testSource": "batch-bug-report-2025-09-10T18:08:03.770234"}}\n--===============6157900518903741992==--\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: content-type: multipart/mixed; boundary=batch_1Xu3WVRptpQ8frvTXujiHeDkN2sKTlfz
header: Vary: Origin, X-Origin
header: Expires: Mon, 01 Jan 1990 00:00:00 GMT
header: Pragma: no-cache
header: Cache-Control: no-cache, no-store, max-age=0, must-revalidate
header: Date: Thu, 11 Sep 2025 01:08:07 GMT
header: Content-Encoding: gzip
header: Server: ESF
header: X-XSS-Protection: 0
header: X-Frame-Options: SAMEORIGIN
header: X-Content-Type-Options: nosniff
header: Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
header: Transfer-Encoding: chunked

[FAILED] Request ID: 1
Exception: <HttpError 400 when requesting https://www.googleapis.com/drive/v3/files/17XMCxQSTXfOAcn5k9CdboRKjLqeUzhb4a3Z7f0f7rWg/modifyLabels?alt=json returned "Bad Request". Details: "[{'message': 'Bad Request', 'domain': 'global', 'reason': 'badRequest'}]">


[FAILED] Request ID: 2
Exception: <HttpError 400 when requesting https://www.googleapis.com/drive/v3/files/1Hrxhm1pl9h9OFVgt3Ribj28EgMYRB4nS7HnJr0A5mLM?supportsAllDrives=true&alt=json returned "Bad Request". Details: "[{'message': 'Bad Request', 'domain': 'global', 'reason': 'badRequest'}]">

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions