title: multipart/x-mixed-replace PNG stream always showing frame before last
body: <p>Having made a program which streams PNG images to the browser by means of a <code>multipart/x-mixed-replace</code> <code>Content-Type</code> header, I noticed that only the frame before-last is displayed in the <code>&lt;img&gt;</code> tag, as opposed to the most recently sent one.</p>
  <p>This behaviour is very annoying, as I'm only sending updates when the image changes to save on bandwidth, which means that the wrong frame will be on screen while I'm waiting for it to update.</p>
  <p>Specifically, I am using Brave Browser (based on chromium), but as I have tried with both &quot;shields&quot; up and down, I assume this problem occurs also in other chromium-based browsers at least.</p>
  <p>Searching for the problem yields only one relevant result (and many non-relevant ones) which is <a href="https://www.howtoforge.com/community/threads/multipart-x-mixed-replace-not-working-i-think-its-due-to-buffering-or-caching.73496/" rel="nofollow noreferrer">this</a> HowToForge thread, with no replies. Likewise, I also thought the issue is to do with buffering, but I made sure to flush the buffer to no avail, much alike to the user in the thread. The user does report that it works on one of their servers though and not the other, which then lead me to believe that it may be to do with a specific HTTP header or something along those lines. My first guess was <code>Content-Length</code> because the browser can tell when the image is complete from that, but it didn't seem to have any effect.</p>
  <p>So essentially, my question is: <strong>Is there a way to tell the browser to show the most recent <code>multipart/x-mixed-replace</code></strong> and not the one before? And, if this isn't standard behaviour, what could the cause be?</p>
  <p>And of course, here's the relevant source code, though I imagine this is more of a general HTTP question than one to do with the code:</p>
  <h1>Server</h1>
  <pre><code>package routes
  
  import (
      &quot;crypto/md5&quot;
      &quot;fmt&quot;
      &quot;image/color&quot;
      &quot;net/http&quot;
      &quot;time&quot;
  
      brain &quot;path/to/image/generator/module&quot;
  )
  
  func init() {
      RouteHandler{
          function: func(w http.ResponseWriter, r *http.Request) {
              w.Header().Set(&quot;Content-Type&quot;, &quot;multipart/x-mixed-replace; boundary=frame&quot;)
              w.Header().Set(&quot;Cache-Control&quot;, &quot;no-cache&quot;) // &lt;- Just in case
              w.WriteHeader(200)
  
              // If the request contains a token and the token maps to a valid &quot;brain&quot;, start consuming frames from
              // the brain and returning them to the client
              params := r.URL.Query()
              if val, ok := params[&quot;token&quot;]; ok &amp;&amp; len(val) &gt; 0 {
                  if b, ok := SharedMemory[&quot;brains&quot;].(map[string]*brain.Brain)[val[0]]; ok &amp;&amp; !b.CheckHasExit() {
                      // Keep a checksum of the previous frame to avoid sending frames which haven't changed. Frames cannot
                      // be compared directly (at least efficiently) as they are slices not arrays
                      previousFrameChecksum := [16]byte{}
  
                      for {
                          if !b.CheckHasExit() {
                              frame, err := b.GetNextFrame(SharedMemory[&quot;conf&quot;].(map[string]interface{})[&quot;DISPLAY_COL&quot;].(color.Color))
                              if err == nil &amp;&amp; md5.Sum(frame) != previousFrameChecksum {
                                  // Only write the frame if we succesfully read it and it's different to the previous
                                  _, err = w.Write([]byte(fmt.Sprintf(&quot;--frame\r\nContent-Type: image/png\r\nContent-Size: %d\r\n\r\n%s\r\n&quot;, len(frame), frame)))
                                  if err != nil {
                                      // The client most likely disconnected, so we should end the stream. As the brain still exists, the
                                      // user can re-connect at any time
                                      return
                                  }
                                  // Update the checksum to this frame
                                  previousFrameChecksum = md5.Sum(frame)
                                  // If possible, flush the buffer to make sure the frame is sent ASAP
                                  if flusher, ok := w.(http.Flusher); ok {
                                      flusher.Flush()
                                  }
                              }
                              // Limit the framerate to reduce CPU usage
                              &lt;-time.After(time.Duration(SharedMemory[&quot;conf&quot;].(map[string]interface{})[&quot;FPS_LIMITER_INTERVAL&quot;].(int)) * time.Millisecond)
                          } else {
                              // The brain has exit so there is no more we can do - we are braindead :P
                              return
                          }
                      }
                  }
              }
          },
      }.Register(&quot;/stream&quot;, &quot;/stream.png&quot;)
  }
  
  </code></pre>
  <h1>Client (<code>start()</code> runs in body <code>onload</code>)</h1>
  <pre><code>function start() {
      // Fetch the token from local storage. If it's empty, the server will automatically create a new one
      var token = localStorage.getItem(&quot;token&quot;);
      // Create a session with the server
      http = new XMLHttpRequest();
      http.open(&quot;GET&quot;, &quot;/startsession?token=&quot;+(token)+&quot;&amp;w=&quot;+(parent.innerWidth)+&quot;&amp;h=&quot;+(parent.innerHeight));
      http.send();
      http.onreadystatechange = (e) =&gt; {
          if (http.readyState === 4 &amp;&amp; http.status === 200) {
              // Save the returned token
              token = http.responseText;
              localStorage.setItem(&quot;token&quot;, token);
              // Create screen
              var img = document.createElement(&quot;img&quot;);
              img.alt = &quot;main display&quot;;
              // Hide the loader when it loads
              img.onload = function() {
                  var loader = document.getElementById(&quot;loader&quot;);
                  loader.remove();
              }
              // Start loading
              img.src = &quot;/stream.png?token=&quot;+token;
              // Start capturing keystrokes
              document.onkeydown = function(e) {
                  // Send the keypress to the server as a command (ignore the response)
                  cmdsend = new XMLHttpRequest();
                  cmdsend.open(&quot;POST&quot;, &quot;/cmd?token=&quot;+(token));
                  cmdsend.send(&quot;keypress:&quot;+e.code);
                  // Catch special cases
                  if (e.code === &quot;Escape&quot;) {
                      // Clear local storage to remove leftover token
                      localStorage.clear();
                      // Remove keypress handler
                      document.onkeydown = function(e) {}
                      // Notify the user
                      alert(&quot;Session ended succesfully and the screen is inactive. You may now close this tab.&quot;);
                  }
                  // Cancel whatever it is the keypress normally does
                  return false;
              }
              // Add screen to body
              document.getElementById(&quot;body&quot;).appendChild(img);
          } else if (http.readyState === 4) {
              alert(&quot;Error while starting the session: &quot;+http.responseText);
          }
      }
  }
  </code></pre>