Skip to content

fix(reports): read file as binary so content and content size matches#12410

Open
XDjackieXD wants to merge 1 commit into
inventree:masterfrom
XDjackieXD:fix/template_size
Open

fix(reports): read file as binary so content and content size matches#12410
XDjackieXD wants to merge 1 commit into
inventree:masterfrom
XDjackieXD:fix/template_size

Conversation

@XDjackieXD

@XDjackieXD XDjackieXD commented Jul 16, 2026

Copy link
Copy Markdown

When reading files as text, the file size reported by python seems to mismatch the actual content size (for example 1458 instead of 1460 in inventree_transfer_order_report.html) which leads to Content-Length header being set to the wrong value when using S3 as the storage backend.

As this only seems to be happening with this file it might also be some weirdness with this specific template but reading in binary mode seems to be less error-prone in general.

When reading files as text, the file size reported byht python seems
to mismatch the actual content size (for example 1458 instead of 1460
in inventree_transfer_order_report.html) which leads to
Content-Length header being set to the wrong value when using S3
as the storage backend.
@netlify

netlify Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy Preview for inventree-web-pui-preview canceled.

Name Link
🔨 Latest commit 70a4195
🔍 Latest deploy log https://app.netlify.com/projects/inventree-web-pui-preview/deploys/6a595bf5c13864000840096f

@SchrodingersGat

Copy link
Copy Markdown
Member

That's an odd one. Can you please add some regression tests with some golden files

@XDjackieXD

Copy link
Copy Markdown
Author

To be honest: I have no idea of django or the used testing framework. The "golden sample" seems to be inventree_transfer_order_report.html in the 1.4.2 tree.

To find this I patched the file_from_template function as follows:

    def file_from_template(self, dir_name: str, file_name: str) -> ContentFile:
        """Construct a new ContentFile from a template file.

        Args:
            dir_name (str): The name of the directory containing the template
            file_name (str): The name of the template file
        Returns:
            ContentFile: The ContentFile object containing the template
        """
        logger.info('Creating %s template file: %s', dir_name, file_name)
        file = get_base_dir().joinpath('report', 'templates', dir_name, file_name)
        if not file.exists():
            raise FileNotFoundError(
                'Template file %s does not exist in %s', file_name, file
            )
        contentFile = ContentFile(file.open('r').read(), os.path.basename(file_name))
        fileContent = contentFile.file
        fileContent.seek(0, os.SEEK_END)
        logger.warning('file_from_template %s length %d', file_name, fileContent.tell())
        fileContent.seek(0, 0)
        return contentFile

The output for inventree_transfer_order_report.html looks like this: 2026-07-16 22:21:53,249 WARNING {'event': 'file_from_template inventree_transfer_order_report.html length 1458', 'timestamp': '2026-07-16T22:21:53.249149Z', 'logger': 'inventree', 'level': 'warning'}
While the file definitely has 1460 byte.
Patching it to rb resulted in this output also printing 1460.

I'd be glad if you could write the tests for this.
If you want to I can take a closer look at the file and try to find a minimal reproducer.

@XDjackieXD

XDjackieXD commented Jul 16, 2026

Copy link
Copy Markdown
Author

I found the issue: there is a unicode symbol in line 13 of inventree_transfer_order_report.html which apparently does not play well with the length reporting. So the returned length is the length in characters, not in bytes which is wrong for a HTTP content-length header.

To be fair I'm not even sure now if this is a proper fix then but any other potential fix would probably need to be done within django-storage while doing it this way should not break anything and lead to a quick solution.

@matmair

matmair commented Jul 17, 2026

Copy link
Copy Markdown
Member

That can be OS and deployment specific; git (project created on Unix like systems) and windows for example often has issues

'Template file %s does not exist in %s', file_name, file
)
return ContentFile(file.open('r').read(), os.path.basename(file_name))
return ContentFile(file.open('rb').read(), os.path.basename(file_name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binary read means we have to handle text encoding issues downstream ourselves; have you confirmed this does not cause any other issues with non-English languages

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I have not confirmed that.

@SchrodingersGat

Copy link
Copy Markdown
Member

How does this result in the wrong content length? Shouldn't any files be served directly by a proxy? Django should not be getting in the way of this. Unless I'm misunderstanding the chain of events here

@matmair

matmair commented Jul 17, 2026

Copy link
Copy Markdown
Member

Depends on the specific action; when the content is accessed via the Django storage framework there are some headers set that might be influenced by the perceived and saved size. There is not much actual reproduction info included so I can not make more concrete statements about the actual path for this to occur

@XDjackieXD

XDjackieXD commented Jul 17, 2026

Copy link
Copy Markdown
Author

How does this result in the wrong content length? Shouldn't any files be served directly by a proxy? Django should not be getting in the way of this. Unless I'm misunderstanding the chain of events here

Not the Content-Length when served to the client but the Content-Length when Inventree tries to upload the files to S3 when the storage backend is set to S3 (I have not checked if the content length to the client would be wrong when served through inventree but the file with issues is never directly served to clients so even if that would be an issue 99.9% of users would have never hit such an issue on the client facing side).

It's weird to me that a seek to the end would not return the length in bytes but the length in characters when the file is opened in text mode but apparently that is how python behaves. I'm by no means a python expert but I also could not find any other way to get the size of a File object in python which is probably why django-storages (or boto3?) uses this method to get the size of the file.

@XDjackieXD

Copy link
Copy Markdown
Author

Looking a bit more into django, django-storage and boto3 it is still not clear to me where this "mishap" regarding the filesize happens.
Django ContentFiles will certainly report the wrong size (as their size is just a len(content): https://github.com/django/django/blob/main/django/core/files/base.py#L132 but their documentation states that size is in bytes which does not reflect the actual behaviour...) but the content length seems to come from botocore (https://github.com/boto/botocore/blob/develop/botocore/utils.py#L3225) which also just does a len(content) which is also wrong if the content is passed to boto3 as anything but binary data (or any format that guarantees that 1 character = 1 byte).
django-storages seems to be doing a conversion to bytes though before passing data to boto3 (https://github.com/jschneier/django-storages/blob/master/storages/backends/s3.py#L231)?
So why isn't boto3 getting the right content_length?

Sadly it seems to be very much not standardized if the size property should represent the length in characters or the size in bytes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants