colour-science / flask-compress

Compress responses of your Flask application.
MIT License
117 stars 27 forks source link

add original content length header #39

Open Alex-ley opened 1 year ago

Alex-ley commented 1 year ago

Feature request:

If you would be open to it, I can also do a PR for it? But I didn't want to assume it would be accepted and do the PR without a discussion first.

    def after_request(self, response):
        app = self.app or current_app

        vary = response.headers.get('Vary')
        if not vary:
            response.headers['Vary'] = 'Accept-Encoding'
        elif 'accept-encoding' not in vary.lower():
            response.headers['Vary'] = '{}, Accept-Encoding'.format(vary)

        accept_encoding = request.headers.get('Accept-Encoding', '')
        chosen_algorithm = self._choose_compress_algorithm(accept_encoding)

        if (chosen_algorithm is None or
            response.mimetype not in app.config["COMPRESS_MIMETYPES"] or
            response.status_code < 200 or
            response.status_code >= 300 or
            (response.is_streamed and app.config["COMPRESS_STREAMS"] is False)or
            "Content-Encoding" in response.headers or
            (response.content_length is not None and
             response.content_length < app.config["COMPRESS_MIN_SIZE"])):
            return response

        response.direct_passthrough = False

        # add original content length for clients that want to update a download progress bar with
        # the progress event of the `XMLHttpRequest` instance
        # https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/progress_event
        # https://github.com/axios/axios/issues/1591#issuecomment-431400903
        # https://stackoverflow.com/a/42345816/9792594 (non-ideal approximation)
        response.headers["Original-Content-Length"] = response.content_length

        if self.cache is not None:
            key = self.cache_key(request)
            compressed_content = self.cache.get(key)
            if compressed_content is None:
                compressed_content = self.compress(app, response, chosen_algorithm)
            self.cache.set(key, compressed_content)
        else:
            compressed_content = self.compress(app, response, chosen_algorithm)

        response.set_data(compressed_content)

        response.headers['Content-Encoding'] = chosen_algorithm
        response.headers['Content-Length'] = response.content_length # compressed length

        # "123456789"   => "123456789:gzip"   - A strong ETag validator
        # W/"123456789" => W/"123456789:gzip" - A weak ETag validator
        etag = response.headers.get('ETag')
        if etag:
            response.headers['ETag'] = '{0}:{1}"'.format(etag[:-1], chosen_algorithm)

        return response
alexprengere commented 1 year ago

From what I understand this is not standard right? More of a workaround for more accurate progress bars when downloading compressed content?

Alex-ley commented 1 year ago

@alexprengere yeah exactly. Sometimes the Brotli compression factor can be huge (which is great) but then any approximation of the compression factor is way off and the progress bar is almost useless. We’re using flask-compress at my start-up and we’re now adding this custom header to improve that. We’re sending huge files around so progress bars are really important.

I realize it’s non standard so that’s why I asked before doing a PR.

If you are open to it, I could put it behind a flag that is by default False/None and also potentially allow the user to chose the naming of this non standard header. So for most users they wouldn’t notice anything or be affected by it.

Of course, understand if you don’t want it in your library. Just thought I’d offer as we can’t be the only people who will face this challenge.