Troubleshooting JSONDecodeError in Python script

Greetings:

Our team is migrating files from an Airtable field to a SeaTable column using a Python script. After working properly for over 500 records, the following error appears on all remaining rows, which we do not understand, in the line:

resultado.append(base.upload_bytes_file(datoArchivo["name"], respuesta.content, replace=False))

An exception occurred: JSONDecodeError Invalid control character at: line 1 column 26 (char 25) json.decoder.JSONDecodeError: Invalid control character at: line 1 column 26 (char 25)

Things we have tried:

  • Check if the problem occurs in a specific Airtable record due to a strange character. We have tried with different records and the same error appears.
  • Check if there is any limit in the API that prevents file upload to SeaTable. It does not seem to be on the list of limits.
  • Check if it is a problem with the file size. It has worked with 500 records with sizes from Kb to Mb without any problem.

We have copied the whole loop in case the context helps. Thanks for your advice.

for indice, elementoAT in enumerate(listaTablasAT):
    print("Analizando tabla "+elementoAT["nombreTablaAT"]+"...")
    for regAT in elementoAT["tablaAT"]:
        campoAT = limpiarCadena(regAT.get("fields").get(elementoAT["campoComparacionAT"]))
        filasTablaST = listaTablasST[indice].get("tablaST")
        camposST = filasTablaST.keys()
        for campoST in camposST:
            if campoAT == campoST:
                # Coincidencia para copiar archivos del registro
                tabla = listaTablasST[indice].get("nombreTablaST")
                idFila = filasTablaST[campoST].get("_id")
                if esPrimeraCopia:
                    # Preparar enlace para subir archivo a SeaTable
                    # Paso 1: Conseguir enlace para carga
                    # Paso 2: Carga del archivos en la nube ST
                    # Paso 3: Actualizar celda con la referencia a los archivos cargados
                    # Paso 1
                    enlace = base.get_file_upload_link()
                    datosFila = datosArchivo(regAT, nombreCampoAT, enlace)
                    resultado = []
                    for datoArchivo, respuesta in zip(datosFila[0], datosFila[1]):
                        # Paso 2
                        print("Copiando archivos de "+campoAT+".")
                        resultado.append(base.upload_bytes_file(datoArchivo["name"], respuesta.content, replace=False))
                        # Si el nombre del archivo se ha actualizado por haber otro archivo con el mismo nombre, actualizamos en la lista
                        nuevoNombre = resultado[-1]["name"]
                        url = datoArchivo["url"]
                        datoArchivo["url"] = url.replace(datoArchivo["name"], nuevoNombre)
                        datoArchivo["name"] = nuevoNombre
                    # Paso 3
                    info = base.update_row(tabla, idFila, {nombreCampoAT: datosFila[0]})
                    break # Termina en la primera coincidencia
                else:
                    # Solo copiamos si el campo está vacío
                    if filasTablaST[campoST][nombreCampoAT] == None:
                        # Preparar enlace para subir archivo a SeaTable
                        # Paso 1: Conseguir enlace para carga
                        # Paso 2: Carga de archivos en la nube ST
                        # Paso 3: Actualizar celda con la referencia a los archivos cargados
                        # Paso 1
                        enlace = base.get_file_upload_link()
                        datosFila = datosArchivo(regAT, nombreCampoAT, enlace)
                        resultado = []
                        for datoArchivo, respuesta in zip(datosFila[0], datosFila[1]):
                            # Paso 2
                            print("Copiando archivos de "+campoAT+".")
                            resultado.append(base.upload_bytes_file(datoArchivo["name"], respuesta.content, replace=False))
                            # Si el nombre del archivo se ha actualizado por haber otro archivo con el mismo nombre, actualizamos en la lista
                            nuevoNombre = resultado[-1]["name"]
                            url = datoArchivo["url"]
                            datoArchivo["url"] = url.replace(datoArchivo["name"], nuevoNombre)
                            datoArchivo["name"] = nuevoNombre
                        # Paso 3
                        info = base.update_row(tabla, idFila, {nombreCampoAT: datosFila[0]})
                        break # Termina en la primera coincidencia
                    else: break

Hi
It seems that the error occured inside the code of function ‘upload_bytes_file’. I check the source code,
the response returned may not a json format. Here is the source code.You can make a debug by copying some of these slices and print out what exactly the result is

    def upload_bytes_file(self, name, content: bytes, relative_path=None, file_type=None, replace=False):
 
        upload_link_dict = self.get_file_upload_link()
        parent_dir = upload_link_dict['parent_path']
        upload_link = upload_link_dict['upload_link'] + '?ret-json=1'
        if not relative_path:
            if file_type and file_type not in ['image', 'file']:
                raise Exception('relative or file_type invalid.')
            if not file_type:
                file_type = 'file'
            relative_path = '%ss/%s' % (file_type, str(datetime.today())[:7])
        else:
            relative_path = relative_path.strip('/')
        response = requests.post(upload_link, data={
            'parent_dir': parent_dir,
            'relative_path': relative_path,
            'replace': 1 if replace else 0
        }, files={
            'file': (name, io.BytesIO(content))
        }, timeout=self.timeout)
        # This may the  error point, please print response.content to check out what the output is
        d = response.json()[0].  
        url = '%(server)s/workspace/%(workspace_id)s/asset/%(dtable_uuid)s/%(relative_path)s/%(filename)s' % {
            'server': self.server_url.strip('/'),
            'workspace_id': self.workspace_id,
            'dtable_uuid': str(UUID(self.dtable_uuid)),
            'file_type': file_type,
            'relative_path': parse.quote(relative_path.strip('/')),
            'filename': parse.quote(d.get('name', name))
        }
        return {
            'type': file_type,
            'size': d.get('size'),
            'name': d.get('name'),
            'url': url
        }

Hello r,

Thank you very much for your response.

We have debugged further and have found where the error is occurring. The error occurs in the previous loop when the following line is executed:

respuesta = requests.get(doc["url"])

The complete code for the function is as follows:

def datosArchivo(regAT, nombreCampoAT, enlace):
    """ 
    Prepara el paquete de archivos para cargar en la fila de SeaTable.
    
    Argumentos:
    regAT (dict): Registro de Airtable.
    nombreCampoAT (str): Nombre del campo de Airtable que contiene los archivos.
    enlace (dict): Enlace de carga para los archivos.
    
    Devuelve:
    tupla: Tupla con la información de los archivos y las respuestas de las peticiones.
    """
    listaDocumentos = regAT["fields"].get(nombreCampoAT)
    infoArchivos = []
    respuestas = []
    if listaDocumentos:
        # Preparar enlace de carga
        relative_path = 'files/' + datetime.now().strftime('%Y-%m')
        data = {
            'parent_dir': enlace["parent_path"],
            'relative_path': relative_path,
        }
        for doc in listaDocumentos:
            url = server_url.rstrip('/') + '/workspace/' + str(workspace_id) + data["parent_dir"] + '/' + relative_path + '/' + doc["filename"]
            info = {
                "name": doc["filename"],
                "size": doc["size"],
                "type": "file",
                "url": url,
            }
            respuesta = requests.get(doc["url"])
            # datos = json.loads(respuesta.text)
            infoArchivos.append(info)
            respuestas.append(respuesta)
    return (infoArchivos, respuestas)

When we checked the value of the response, we found that it is not JSON-parseable.

We have continued investigating and now think that there may be a limitation in Airtable regarding the expiration of file links.

If anyone has experienced a similar situation, we would appreciate knowing how they resolved it without downloading the files locally.

Happy New Year.