4

I want to reload an image on a page if it has been updated on the server. In other questions it has been suggested to do something like

newImage.src = "http://localhost/image.jpg?" + new Date().getTime();

to force the image to be re-loaded, but that means that it will get downloaded again even if it really hasn't changed.

Is there any Javascript code that will cause a new request for the same image to be generated with a proper If-Modified-Since header so the image will only be downloaded if it has actually changed?

UPDATE: I'm still confused: if I just request the typical URL, I'll get the locally cached copy. (unless I make the server mark it as not cacheable, but I don't want to do that because the whole idea is to not re-download it unless it really changes.) if I change the URL, I'll always re-download, because the point of the new URL is to break the cache. So how do I get the in-between behavior I want, i.e. download the file only if it doesn't match the locally cached copy?

Community
  • 1
  • 1
David Maymudes
  • 5,664
  • 31
  • 34
  • if the server is properly configured (i.e. supports Etag/Last-Modified), the client/proxy/cache layer should conform to what the server sent. – jldupont Dec 14 '09 at 19:45
  • not sure what this comment means--are you saying that if the server is correctly configured that the image should refresh if I just re-set the src to the original image URL and don't add the extra fake timestamp at the end? – David Maymudes Dec 14 '09 at 19:58
  • If the server is configured correctly, then when the client requests a resource with the "If-Modified-Since" header, the server will respond with a "304" status code. In other words, it is not sufficient for the client to ask with "If-Modified-Since" header, the server must play ball too. – jldupont Dec 14 '09 at 20:24
  • right, but how do I get the client to generate a request with the if-modified-since header? if I change the URL, the client thinks it's a new file, so it doesn't add the header, but if I don't change the URL, there's no way to ask the page to do a soft refresh of the image, at least not one I know.... – David Maymudes Dec 14 '09 at 20:26
  • normally you wouldn't: if the server sent along an "Etag/Last-Modified" header, the client would then leverage that information on subsequent calls to the server. That's what I meant by "the two must play ball". Of course you can always craft Ajax XmlHttpRequests with this header information and see for yourself. – jldupont Dec 14 '09 at 20:33
  • it just feels like something that must be a pretty common goal, so I'm surprised the Javascript image object doesn't have a "refreshIfChanged" method or something like that. – David Maymudes Dec 14 '09 at 20:34
  • ....because if you have a well engineered system (read: server that respects the standards), you needn't to have some plaster on the JS side. – jldupont Dec 14 '09 at 20:54
  • we're not talking about the server here: how do I write a client web page that will do the right thing, i.e. re-issue the request for the same URL as previous, with a proper if-modified-since header? Maybe the answer is that I have to make a new Javascript image object, but even that seems like by default it should just re-use the locally cached object and not query the server to see if there's been an update, so I must need to do something additional... – David Maymudes Dec 14 '09 at 21:37

9 Answers9

2

Javascript can't listen for an event on the server. Instead, you could employ some form of long-polling, or sequential calls to the server to see if the image has been changed.

Sampson
  • 265,109
  • 74
  • 539
  • 565
  • Actually, by implementing the bayeux protocol, you sort of can listen for an event. Not sure if it's worth it for OP though. – Tatu Ulmanen Dec 14 '09 at 19:49
  • I know I need to poll; basically what I want is the simplest possible way to poll for whether the image has changed, i.e. to send an http request that says "download this image if it's been modified since the one I have", so the question is what Javascript can I use to generate that request. – David Maymudes Dec 14 '09 at 20:08
1

You should have a look at the xhr.setRequestHeader() method. It's a method of any XMLHttpRequest object, and can be used to set headers on your Ajax queries. In jQuery, you can easily add a beforeSend property to your ajax object and set up some headers there.

That being said, caching with Ajax can be tricky. You might want to have a look at this thread on Google Groups, as there's a few issues involved with trying to override a browser's caching mechanisms. You'll need to ensure that your server is returning the proper cache control headers in order to be able to get something like this to work.

zombat
  • 92,731
  • 24
  • 156
  • 164
1

One way of doing this is to user server-sent events to have the server push a notification whenever the image has been changed. For this you need a server-side script that will periodically check for the image having been notified. The server-side script below ensures that the server sends an event at least once every (approximately) 60 seconds to prevent timeouts and the client-side HTML handles navigation away from and to the page:

sse.py

#!/usr/bin/env python3

import time
import os.path

print("Content-Type: text/event-stream\n\n", end="")

IMG_PATH = 'image.jpg'

modified_time = os.path.getmtime(IMG_PATH)
seconds_since_last_send = 0

while True:
    time.sleep(1)
    new_modified_time = os.path.getmtime(IMG_PATH)
    if new_modified_time != modified_time:
        modified_time = new_modified_time
        print('data: changed\n\n', end="", flush=True)
        seconds_since_last_send = 0
    else:
        seconds_since_last_send += 1
        if seconds_since_last_send == 60:
            print('data: keep-alive\n\n', end="", flush=True)
            seconds_since_last_send = 0

And then your HTML would include some JavaScript code:

sse.html

<html>
<head>
   <meta charset="UTF-8">
   <title>Server-sent events demo</title>
</head>
<body>
  <img id="img" src="image.jpg">

<script>
  const img = document.getElementById('img');
  let evtSource = null;

  function setup_sse()
  {
    console.log('Creating new EventSource.');
    evtSource = new EventSource('sse.py');

    evtSource.onopen = function() {
      console.log('Connection to server opened.');
    };

    // if we navigate away from this page:
    window.onbeforeunload = function() {
      console.log('Closing connection.');
      evtSource.close();
      evtSource = null;
    };

    evtSource.onmessage = function(e) {
      if (e.data == 'changed')
        img.src = 'image.jpg?version=' + new Date().getTime();
    };

    evtSource.onerror = function(err) {
      console.error("EventSource failed:", err);
    };
  }


  window.onload = function() {
    // if we navigate back to this page:
    window.onfocus = function() {
      if (!evtSource)
        setup_sse();
    };

    setup_sse(); // first time
  };
</script>
</body>
</html>
Booboo
  • 38,656
  • 3
  • 37
  • 60
  • Thank you. But the problem statement constrains the solution to client-side JavaScript. It asks how a client-side script can ensure a new request for the image has the if-modified-since header, so the server will know to reply with 304 when the file is the same as the cached copy. – MetaEd Oct 11 '22 at 19:45
1

Here am loading an image, tree.png, as binary data dynamically with AJAX and saving the Last-Modified header. Periodically (every 5 second in the code below). I issue another download request sending backup a If-Modified-Since header using the saved last-modified header. I check to see if data has been returned and re-create the image with the data if present:

<!doctype html>
<html>
<head>
<title>Test</title>
<script>
window.onload = function() {

  let image = document.getElementById('img');
  var lastModified = ''; // 'Sat, 11 Jun 2022 19:15:43 GMT'

  function _arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array(buffer);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa( binary );
}

  function loadImage()
  {
    var request = new XMLHttpRequest();
    request.open("GET", "tree.png", true);
    if (lastModified !== '')
      request.setRequestHeader("If-Modified-Since", lastModified);
    request.responseType = 'arraybuffer';
    request.onload = function(/* oEvent */) {
      lastModified = request.getResponseHeader('Last-Modified');
      var response = request.response;
      if (typeof response !== 'undefined' && response.byteLength !== 0) {
        var encoded = _arrayBufferToBase64(response);
        image.src = 'data:image/png;base64,' + encoded;
      }
      window.setTimeout(loadImage, 5000);
    };
    request.send();
  }

  loadImage();
};
</script>
</head>
<body>
  <img id="img">
</body>
</html>
Booboo
  • 38,656
  • 3
  • 37
  • 60
  • If I understand right, you have bypassed the browser cache completely. Instead you're caching the image yourself as a Base64 object, aging it yourself, and issuing the periodic request yourself with the appropriate request header. This seems like a good solution, if it works. Please note this solution and a couple of others came in too late for me to fully evaluate before the bounty expires, so I'll have to re-offer a bounty once I am confident I have a good answer. – MetaEd Oct 12 '22 at 17:45
  • This works like a champ. Whenever the timer expires, I see the server request. The reply is a 304, except that when the image is updated on the server side there is one 200 reply and the page updates with the new image. Brilliant. – MetaEd Oct 12 '22 at 19:31
  • I have 23 hours left to award the bounty. If things don't change, this answer will get the award, because it's the only one I've actually been able to reproduce and make work. – MetaEd Oct 12 '22 at 19:32
  • Bounty awarded here. – MetaEd Oct 13 '22 at 16:06
0

You can write a server side method which just returns last modified date of the image resource,
Then you just use polling to check for the modified date and then reload if modified date is greater than previous modified date.

pseudo code (ASP.NET)

//server side ajax method
[WebMethod]
public static string GetModifiedDate(string resource)
{
    string path = HttpContext.Current.Server.MapPath("~" + resource);
    FileInfo f = new FileInfo(path);
    return f.LastWriteTimeUtc.ToString("yyyy-dd-MMTHH:mm:ss", CultureInfo.InvariantCulture);//2020-05-12T23:50:21
}

var pollingInterval = 5000;
function getPathFromUrl(url) {
    return url.split(/[?#]/)[0];
}
function CheckIfChanged() {
    $(".img").each(function (i, e) {
        var $e = $(e);
        var jqxhr = $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "/Default.aspx/GetModifiedDate",
            data: "{'resource':'" + getPathFromUrl($e.attr("src")) + "'}"
        }).done(function (data, textStatus, jqXHR) {
            var dt = jqXHR.responseJSON.d;
            var dtCurrent = $e.attr("data-lastwrite");
            if (dtCurrent) {
                var curDate = new Date(dtCurrent);
                var dtLastWrite = new Date(dt);
                //refresh if modified date is higher than current date
                if (dtLastWrite > curDate) {
                    $e.attr("src", getPathFromUrl($e.attr("src")) + "?d=" + new Date());//fool browser with date querystring to reload image
                }
            }
            $e.attr("data-lastwrite", dt);
        });
    }).promise().done(function () {
        window.setTimeout(CheckIfChanged, pollingInterval);
    });
}
$(document).ready(function () {
    window.setTimeout(CheckIfChanged, pollingInterval);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<img class="img" src="/img/rick.png" alt="rick" />

rick updated image

Nitin Sawant
  • 7,278
  • 9
  • 52
  • 98
  • Thank you. But the problem statement constrains the solution to client-side JavaScript. It asks how a client-side script can ensure a new request for the image has the if-modified-since header, so the server will know to reply with 304 when the file is the same as the cached copy. – MetaEd Oct 11 '22 at 19:44
0

If you are going to check whether files has changed on the server you have to make http request from the server for the file time, because there is no other way for your check the file time once page get loaded to the browser.

So that time check script will like

filetimecheck.php

<?php
    echo filemtime(string $filename);
?>

Then you can check the file time using your Javascript. BTW I have put jQuery $.get for check the file time.

dusplayimage.php

<img id="badge" src="image.jpg"> />
<script>
var image_time = <?php echo filemtime(string $filename); ?>;
var timerdelay = 5000;
function imageloadFunction(){
    $.get("filetimecheck.php", function(data, status){
        console.log("Data: " + data + "\nStatus: " + status);
        if(image_time < parseInt(data)) {
            document.getElementById('yourimage').src = "image.jpg?random="+new Date().getTime();
        }
      });

    setTimeout(imageloadFunction, timerdelay);
}

imageloadFunction();

</script>

You will be using extra call to the server to check the file time which you can't avoid however you can use the time delay to fine-tune the polling time.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Kumara
  • 39
  • 3
  • Thank you. But the problem statement constrains the solution to client-side JavaScript. It asks how a client-side script can ensure a new request for the image has the if-modified-since header, so the server will know to reply with 304 when the file is the same as the cached copy. – MetaEd Oct 11 '22 at 19:47
  • Not really, you can stop the cache using .htaccess. also without checking with the server side you cannot check the modification. – Kumara Oct 15 '22 at 14:59
0

Yes, you can customize this behavior. Even with virtually no change to your client code.

So, you will need a ServiceWorker (caniuse 96.59%).

ServiceWorker can proxy your http requests. Also, ServiceWorker has already built-in storage for the cache. If you have not worked with ServiceWorker, then you need to study it in detail. The idea is the following:

When requesting a picture (in fact, any file), check the cache. If there is no such picture in the cache, send a request and fill the cache storage with the date of the request and the file. If the cache contains the required file, then send only the date and path of the file to the special API to the server. The API returns either the file and modification date at once (if the file was updated), or the response that the file has not changed {"changed": false}. Then, based on the response, the worker either writes a new file to the cache and resolves the request with the new file, or resolves the request with the old file from the cache.

Here is an example code (not working, but for understanding)

s-worker.js

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;

  event.respondWith(
    (async function () {
      const cache = await caches.open('dynamic-v1');
      const cachedResponse = await cache.match(event.request);

      if (cachedResponse) {
        // check if a file on the server has changed
        const isChanged = await fetch('...');
        if (isChanged) {
          // give file, and in the background write to the cache
        } else {
          // return data
        }
        return cachedResponse;
      } else {
        // request data, send from the worker and write to the cache in the background
      }
    })()
  );
});

In any case, look for "ways to cache statics using ServiceWorker" and change the examples for yourself.

Alexander
  • 291
  • 4
  • 13
  • According to https://developer.mozilla.org/en-US/docs/Web/API/Cache, "The caching API doesn't honor HTTP caching headers." Doesn't this mean the fetch will always get a 200, not a 304, meaning it will always retransfer the entire image? Not being familiar with ServiceWorker I don't know the answer to this. – MetaEd Oct 12 '22 at 17:38
  • @MetaEd, I've included this code for reference. If "Caching API" doesn't suit you, then use IndexedDB to store files, it's not hard. The answer answers the question "what to do" rather than "how to do". – Alexander Oct 13 '22 at 12:52
  • Agreed, because the question is how to poll the server periodically for a change, using the if-modified-since header to keep the polling cost down. The storage used on the client side to cache the result is beside the point. – MetaEd Oct 13 '22 at 16:06
-1

WARNING this solution is like taking a hammer to crush a fly

You can use sockets.io to pull information to browser.

In this case you need to monitor image file changes on the server side, and then if change occur emit an event to indicate the file change.

On client (browser) side listen to the event and then then refresh image each time you get the event.

Alaindeseine
  • 3,260
  • 1
  • 11
  • 21
-1

set your image source in a data-src property,

and use javascript to periodicaly set it to the src attribute of that image with a anchor (#) the anchor tag in the url isn't send to the server.

Your webserver (apache / nginx) should respond with a HTTP 304 if the image wasn't changed, or a 200 OK with the new image in the body, if it was

setInterval(function(){ 
  l= document.getElementById('logo'); 
  l.src = l.dataset.src+'#'+ new Date().getTime();
  },1000);
 <img id="logo" alt="awesome-logo" data-src="https://upload.wikimedia.org/wikipedia/commons/1/11/Test-Logo.svg" />

EDIT

Crhome ignores http cache-control headers, for subsequent image reloads.

but the fetch api woks as expected

fetch('https://upload.wikimedia.org/wikipedia/commons/1/11/Test-Logo.svg', { cache: "no-cache" }).then(console.log);

the no-cache instructs the browser to always revalidate with the server, and if the server responds with 304, use the local cached version.

on8tom
  • 1,766
  • 11
  • 24
  • When the timer expires the first time, a request for the image is sent to the server. After that, whenever the timer expires, the DOM is updated with the new anchor, but that's all. No request is sent. This is with Chrome ver. 106. – MetaEd Oct 12 '22 at 18:46
  • I have 23 hours left to evaluate the answers and award the original bounty. The virtue of this answer is it's very tiny. But it's not working. – MetaEd Oct 12 '22 at 19:28
  • idd chrome did change it's behaviour. firefox still reloads the image – on8tom Oct 12 '22 at 20:05
  • Using fetch() I believe makes this a duplicate of Alexander's answer. In that case, does it have the issue I raised in my comment on Alexander's answer? – MetaEd Oct 12 '22 at 20:30