Teensy 4.1 QNethernet requests and SD problems

nieuwemaker

Member
A part of my Teensy 4.1 project involves a simple webserver using QNEthernet and SD to physically read files from the SD card. My website is completely served from the Teensy and is located on the SD card. The header of the html looks like this:


<link href="css/app.css" rel="stylesheet"> <!-- GENERIC GLOBAL CSS --> <link href="css/sections/appContainer.css" rel="stylesheet"> <!-- BASE DOCUMENT CSS --> <link href="css/sections/appHeader.css" rel="stylesheet"> <!-- All header markup CSS --> <link href="css/sections/connectTab.css" rel="stylesheet"> <!-- Connect tab CSS --> <link href="css/sections/playTab.css" rel="stylesheet"> <!-- Play tab CSS --> <link href="css/sections/solveTab.css" rel="stylesheet"> <!-- Solve tab CSS --> <!-- JAVASCRIPT --> <script src="http://pixelperfect/js/ipaddress.js"></script> <!-- Controller generated content to prefill data fields --> <script src="js/generic.js"></script> <!-- Generic functions and GLOBALS that are used throughout the APP--> <script src="js/plugins/svg.js"></script> <!-- little SVG library helping me to draw --> <!-- JAVASCRIPT :: model classes --> <script src="js/models/PubSubBase.js"></script> <script src="js/models/Clip.js"></script> <script src="js/models/Channel.js"></script> <script src="js/models/Bank.js"></script> <script src="js/models/Project.js"></script> <script src="js/models/Animation.js"></script> <script src="js/models/Sequencer.js"></script> <!-- JAVASCRIPT :: web components --> <script type="module" src="js/components/toggle-group.js"></script> <script type="module" src="js/components/vertical-slider.js"></script> <script type="module" src="js/components/channel-header.js"></script> <script type="module" src="js/components/clip-view.js"></script> <script type="module" src="js/components/animation-select-modal.js"></script> <script type="module" src="js/components/clip-min-view.js"></script> <script type="module" src="js/components/channel-min-view.js"></script> <script type="module" src="js/components/bank-min-view.js"></script> <script type="module" src="js/components/channel-view.js"></script> <script type="module" src="js/components/bank-select-view.js"></script> <script type="module" src="js/components/bank-view.js"></script> <script type="module" src="js/components/quick-clips-view.js"></script> <script type="module" src="js/components/project-view.js"></script> <script type="module" src="js/components/sequencer-view.js"></script> <script type="module" src="js/components/performance-view.js"></script> <script type="module" src="js/components/status-bar.js"></script> <script type="module" src="js/components/color-select-view.js"></script> <script type="module" src="js/components/default-video-view.js"></script> <!-- JAVASCRIPT :: Manager classes --> <script src="js/models/managers/LocalStorageManager.js"></script> <script src="js/models/managers/AnimationManager.js"></script> <script src="js/models/managers/ControllerManager.js"></script> <script src="js/models/managers/ProjectManager.js"></script> <script src="js/models/managers/MidiManager.js"></script> <script src="js/main.js"></script> <!-- main() function to start APP -->

As the example shows, a lot of files are requested from the SD card and this is where the weird stuff begins. If I only request 1 file it loads perfectly well, but when all these files are requested some files load perfectly, others stop loading at a random amount of lines and even weirder, some code even gets mixed up between files. So CSS code is mixed through JS code for example.

To mitigate the problem I've added a delayMicroseconds(100) after every line that is read and that works! This looks like this:
while (sdFileHandle.available()) { client.write(sdFileHandle.read()); // stream file per byte to http client delayMicroseconds(100); // was experiment to see if files are loaded completely this way ... }

This is complete Bonkers right? The result is that it takes 25 seconds to load the page for 300kb of data, which is unacceptable for my usecase. So what is going on? Teensy cannot multi-task right, so although the browser requests multiple files at the same time, Teensy handles them in a serial order right? And why would that delay even matter? Am I reading too fast from the SD card? Is that even possible using the sdFileHandle.available() that is used in every example?

To provide some more context I will add snippets from the rest of my code so you can see what I do:

Every 5 milliseconds this method is called from the main loop to check if a web-request is being done:
void WebserverController::HandleClient(){ this->client = this->server->available(); if(this->client && this->IsConnected() && this->client.available()){ // step 1: read what the client is requesting // code here to do so // step 2: if it is a request for a weburl this->servePage(getUrl); // --> this handles the web url request this->client.stop(); // saves memory to quit after each request } }

Then it goes to the servePage() method to handle the file that is requested. It uses the standard fileHandler to read the file and send the input directly to the client stream.
void WebserverController::servePage(String url){ if(url == "/"){ url = "/index.html"; } // redirect Root to index String sdName = SD_FILE_PATH + url.substring(1); // get rid of the "/" in front File sdFileHandle = this->sdController->OpenForRead(sdName); if(sdFileHandle){ // The file exists and can be opened String fileType = url.substring(url.indexOf(".")+1); this->sendCorrectHeader(fileType.toLowerCase()); while (sdFileHandle.available()) { client.write(sdFileHandle.read()); // stream file per byte to http client delayMicroseconds(100); // was experim,ent to see if files are loaded completely this way ... } sdFileHandle.close(); client.closeOutput(); } else { // requested source not found. Let's send a friendly message this->sendHTMLHeader(RESPONSE_NOT_FOUND,MIME_HTML); this->client.printf("<html><body>requested HTML document <b>%s</b> does not exist</body></html>",sdName.c_str()); } }

I use the SD.open() function to open a READ handle for the file like this:
File SDController::OpenForRead(String filename){ if(this->initialized){ return SD.open(filename.c_str(),FILE_READ); } return NULL; }

Hopefully someone here understands what is happening here and can help me out. As said. It works, but is very slow and I hate it if things work without knowing why.
 
Side note and pro tip: you can put code inside code blocks with the “</>” button. This makes a “CODE” tag instead of an “ICODE” or font tag, and puts code inside a box instead of making it look like individual and separate (and wrapped) lines.

This also makes it so the lines don’t wrap, making the code much easier to read. I’m currently reading this on my phone and almost all of the lines are wrapped.
 
Last edited:
@shawn thanks for the tip. I was beyond the edit time, so I post it again.

A part of my Teensy 4.1 project involves a simple webserver using QNEthernet and SD to physically read files from the SD card. My website is completely served from the Teensy and is located on the SD card. The header of the html looks like this:
HTML:
        <link href="css/app.css"  rel="stylesheet">                  <!-- GENERIC GLOBAL CSS -->
        <link href="css/sections/appContainer.css" rel="stylesheet"> <!-- BASE DOCUMENT CSS -->
        <link href="css/sections/appHeader.css" rel="stylesheet">    <!-- All header markup CSS -->
        <link href="css/sections/connectTab.css" rel="stylesheet">   <!-- Connect tab CSS -->
        <link href="css/sections/playTab.css" rel="stylesheet">      <!-- Play tab CSS -->
        <link href="css/sections/solveTab.css" rel="stylesheet">     <!-- Solve tab CSS -->
        <!-- JAVASCRIPT -->
         <script src="http://pixelperfect/js/ipaddress.js"></script>  <!-- Controller generated content to prefill data fields -->
        <script src="js/generic.js"></script>     <!-- Generic functions and GLOBALS that are used throughout the APP-->
        <script src="js/plugins/svg.js"></script> <!-- little SVG library helping me to draw -->
        <!-- JAVASCRIPT :: model classes -->
        <script src="js/models/PubSubBase.js"></script>
        <script src="js/models/Clip.js"></script>
        <script src="js/models/Channel.js"></script>
        <script src="js/models/Bank.js"></script>
        <script src="js/models/Project.js"></script>
        <script src="js/models/Animation.js"></script>
        <script src="js/models/Sequencer.js"></script>
        <!-- JAVASCRIPT :: web components -->
        <script type="module" src="js/components/toggle-group.js"></script>
        <script type="module" src="js/components/vertical-slider.js"></script>
        <script type="module" src="js/components/channel-header.js"></script>
        <script type="module" src="js/components/clip-view.js"></script>
        <script type="module" src="js/components/animation-select-modal.js"></script>
        <script type="module" src="js/components/clip-min-view.js"></script>
        <script type="module" src="js/components/channel-min-view.js"></script>
        <script type="module" src="js/components/bank-min-view.js"></script>
        <script type="module" src="js/components/channel-view.js"></script>
        <script type="module" src="js/components/bank-select-view.js"></script>
        <script type="module" src="js/components/bank-view.js"></script>
        <script type="module" src="js/components/quick-clips-view.js"></script>
        <script type="module" src="js/components/project-view.js"></script>
        <script type="module" src="js/components/sequencer-view.js"></script>
        <script type="module" src="js/components/performance-view.js"></script>
        <script type="module" src="js/components/status-bar.js"></script>
        <script type="module" src="js/components/color-select-view.js"></script>
        <script type="module" src="js/components/default-video-view.js"></script>
        <!-- JAVASCRIPT :: Manager classes -->
        <script src="js/models/managers/LocalStorageManager.js"></script>
        <script src="js/models/managers/AnimationManager.js"></script>
        <script src="js/models/managers/ControllerManager.js"></script>
        <script src="js/models/managers/ProjectManager.js"></script>
        <script src="js/models/managers/MidiManager.js"></script>
        <script src="js/main.js"></script> <!-- main() function to start APP -->

As the example shows, a lot of files are requested from the SD card and this is where the weird stuff begins. If I only request 1 file it loads perfectly well, but when all these files are requested some files load perfectly, others stop loading at a random amount of lines and even weirder, some code even gets mixed up between files. So CSS code is mixed through JS code for example.
To mitigate the problem I've added a delayMicroseconds(100) after every line that is read and that works! This looks like this:
C++:
while (sdFileHandle.available()) {
        client.write(sdFileHandle.read()); // stream file per byte to http client
        delayMicroseconds(100); // was experiment to see if files are loaded completely this way ...
 }

This is complete Bonkers right? The result is that it takes 25 seconds to load the page for 300kb of data, which is unacceptable for my usecase. So what is going on? Teensy cannot multi-task right, so although the browser requests multiple files at the same time, Teensy handles them in a serial order right? And why would that delay even matter? Am I reading too fast from the SD card? Is that even possible using the sdFileHandle.available() that is used in every example?
To provide some more context I will add snippets from the rest of my code so you can see what I do:
Every 5 milliseconds this method is called from the main loop to check if a web-request is being done:
C++:
void WebserverController::HandleClient(){
    this->client = this->server->available();
    if(this->client && this->IsConnected() && this->client.available()){
        // step 1: read what the client is requesting
       // code here to do so
        // step 2: if it is a request for a weburl
        this->servePage(getUrl); // --> this handles the web url request
        this->client.stop(); // saves memory to quit after each request
    }
}

Then it goes to the servePage() method to handle the file that is requested. It uses the standard fileHandler to read the file and send the input directly to the client stream.
C++:
void WebserverController::servePage(String url){
    if(url == "/"){ url = "/index.html"; } // redirect Root to index
        String sdName     = SD_FILE_PATH + url.substring(1); // get rid of the "/" in front
        File sdFileHandle = this->sdController->OpenForRead(sdName);
        if(sdFileHandle){ // The file exists and can be opened
            String fileType = url.substring(url.indexOf(".")+1);
            this->sendCorrectHeader(fileType.toLowerCase());
            while (sdFileHandle.available()) {
                client.write(sdFileHandle.read()); // stream file per byte to http client
                delayMicroseconds(100); // was experim,ent to see if files are loaded completely this way ...
            }
            sdFileHandle.close();
            client.closeOutput();
        } else { // requested source not found. Let's send a friendly message
            this->sendHTMLHeader(RESPONSE_NOT_FOUND,MIME_HTML);
            this->client.printf("<html><body>requested HTML document <b>%s</b> does not exist</body></html>",sdName.c_str());
        }
}

I use the SD.open() function to open a READ handle for the file like this:
C++:
File SDController::OpenForRead(String filename){
    if(this->initialized){
        return SD.open(filename.c_str(),FILE_READ);
    }
    return NULL;
}

Hopefully someone here understands what is happening here and can help me out. As said. It works, but is very slow and I hate it if things work without knowing why.
 
As an update to this: Copilot suggested I needed to use multiple EthernetClient instances called client (at the moment it is a property of the class) But this did not solve the problem. It suggested I had a concurrency problem, overwriting the EthernetClient and thus mixing up the responses. After re-writing this part it had exactly the same result though ... even when every request was contained in its own EthernetClient. This made me think, that perhaps SD is not returning unique handles to files?
 
Last edited:
That helps, thank you.

I’m going to sound cliché here, but could you please provide a complete and minimal program that demonstrates the problem? I’d like to see your server code.

Question: did you write the server in a similar way to what’s in the QNEthernet examples?
 
I was hoping this was not needed, but creating a minimalistic version could be the only way to debug the problem without having all other components of the software present. I did write it in a similar way as the Examples show with the difference that I use class instances. I also use QNEthernet for REST (GET & POST) / ARTNET and TPM2.NET communication on the same Teensy and this all works perfectly fine.
 
I did a simple test to check if the files are really read in a serial fashion and not in parallel, and I can confirm this. It does not fix my problem yet, but at least one question has been answered.
 
Hi nieuwemaker, I am having very similar issues serving multiple files, no just from SD card but also from flash.

I have created the following minimalist webserver using the ServerWithListeners example by Shawn as the starting point.

It is working reliably for me, however in the much more complex program I moved this code from I am having reliability issues.

I have added a lot of debug prints and timing/performance outputs for testing purposes.

Definitely can be optimised but something I could get going fairly quickly.



C++:
// Created by Craig Strudwicke 23/1/2026
// Heavily based on ServerWithListeners example by Shawn Silverman <shawn@pobox.com>
//
// Basic web server that serves files from an SD card using QNEthernet and SdFs libraries.
// It supports multiple clients and robust file serving with proper HTTP headers.
// Heavily sprinkled with debug output to Serial for tracing and diagnostics.

// using QNEthernet library 0.33.0
// Only GET Method is supported for file serving from SD card

// C++ includes
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
#include "SD.h"
#include "SdFat.h"
#include "core_pins.h"
#include "stdio.h"
#include "string.h"
#include "SPI.h"

#include <QNEthernet.h>

using namespace qindesign::network;

#define DEBUG_WEB_SERVER 1

// --------------------------------------------------------------------------
//  Configuration
// --------------------------------------------------------------------------
//----------------------------------------------------------------------------------
// SD card defines
//#define ENABLE_DEDICATED_SPI 1 
// Teensy 3.5 & 3.6 & 4.1 on-board: BUILTIN_SDCARD
const int chipSelect = BUILTIN_SDCARD;
const uint8_t SD_CS_PIN = BUILTIN_SDCARD;
#define SPI_CLOCK SD_SCK_MHZ(50)

// Try to select the best SD card configuration.
#if HAS_SDIO_CLASS
#define SD_CONFIG SdioConfig(FIFO_SDIO)
#elif ENABLE_DEDICATED_SPI
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SPI_CLOCK)
#else  // HAS_SDIO_CLASS
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, SHARED_SPI, SPI_CLOCK)
#endif  // HAS_SDIO_CLASS

//#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SPI_CLOCK)

// Set PRE_ALLOCATE true to pre-allocate file clusters.
const bool PRE_ALLOCATE = true;
// Size of read/write.
const size_t BUF_SIZE = 512;
#define SD_FAT_TYPE 3  // 0: SdFat, 1: SdFat32, 2: SdExFat, 3: SdFs

#if SD_FAT_TYPE == 0
SdFat sd;
File file;
File root;
#elif SD_FAT_TYPE == 1
SdFat32 sd;
File32 file;
File32 root;
#elif SD_FAT_TYPE == 2
SdExFat sd;
ExFile file;
ExFile root;
#elif SD_FAT_TYPE == 3
SdFs sd;
FsFile fsFile;
FsFile root;
//#elif
//#error Invalid SD_FAT_TYPE
#endif  // SD_FAT_TYPE


struct ClientState {
  ClientState(EthernetClient client)
      : client(std::move(client)) {}

  EthernetClient client;
  bool closed = false;
  int emptyHeader = true;
  int emptyRequest = true;
    char HTTP_req[512];
  int httpRequestIndex = 0;
 

  // For timeouts.
  uint32_t lastRead = millis();  // Mark creation time

  // For half closed connections, after "Connection: close" was sent
  // and closeOutput() was called
  uint32_t closedTime = 0;    // When the output was shut down
  bool outputClosed = false;  // Whether the output was shut down

  // Parsing state
  bool emptyLine = false;
};



bool serveFile(SdFs& sdfs, EthernetClient* client, const char* path);
void processClientDataAlt(ClientState &state);
bool initEthernet(void);


// NOTE: Not all the code here is needed

// The DHCP timeout, in milliseconds. Set to zero to not wait and
// instead rely on the listener to inform us of an address assignment.
constexpr uint32_t kDHCPTimeout = 15000;  // 15 seconds

// The link timeout, in milliseconds. Set to zero to not wait and
// instead rely on the listener to inform us of a link.
constexpr uint32_t kLinkTimeout = 5000;  // 5 seconds

constexpr uint16_t kServerPort = 80;

// Timeout for waiting for input from the client.
constexpr uint32_t kClientTimeout = 5000;  // 5 seconds

// Timeout for waiting for a close from the client after a
// half close.
constexpr uint32_t kShutdownTimeout = 30000;  // 30 seconds

// Set the static IP to something other than INADDR_NONE (all zeros)
// to not use DHCP. The values here are just examples.
IPAddress staticIP{192, 168, 2, 177};//{192, 168, 1, 101};
IPAddress subnetMask{255, 255, 255, 0};
IPAddress gateway{192, 168, 2, 1};

// Globals
int httpHeaderIndex=0;
char HTTP_header[512];

// --------------------------------------------------------------------------
//  Types
// --------------------------------------------------------------------------

// Keeps track of state for a single client.

// --------------------------------------------------------------------------
//  Program State
// --------------------------------------------------------------------------

// Keeps track of what and where belong to whom.
std::vector<ClientState> clients;

// The server.
EthernetServer server{kServerPort};

const char* getContentTypeSafe(const char* path) {
    if (!path) return "text/plain";

    size_t len = strlen(path);
    
    // Fast suffix checks using C-string comparison
    if (len > 5 && strcasecmp(path + len - 5, ".html") == 0) return "text/html";
    if (len > 4 && strcasecmp(path + len - 4, ".htm") == 0)  return "text/html";
    if (len > 4 && strcasecmp(path + len - 4, ".css") == 0)  return "text/css";
    if (len > 3 && strcasecmp(path + len - 3, ".js") == 0)   return "application/javascript";
    if (len > 5 && strcasecmp(path + len - 5, ".json") == 0) return "application/json";
    if (len > 4 && strcasecmp(path + len - 4, ".png") == 0)  return "image/png";
    if (len > 4 && strcasecmp(path + len - 4, ".jpg") == 0)  return "image/jpeg";
    if (len > 5 && strcasecmp(path + len - 5, ".jpeg") == 0) return "image/jpeg";
    if (len > 4 && strcasecmp(path + len - 4, ".ico") == 0)  return "image/x-icon";
    if (len > 4 && strcasecmp(path + len - 4, ".svg") == 0)  return "image/svg+xml";
    if (len > 5 && strcasecmp(path + len - 5, ".woff") == 0) return "font/woff";
    if (len > 6 && strcasecmp(path + len - 6, ".woff2") == 0) return "font/woff2";

    return "text/plain";
}

bool serveFile(SdFs& sdfs, EthernetClient* client, const char* path) {
 
  int debug = DEBUG_WEB_SERVER;

  uint32_t startRead, stopRead, readDuration, writeDuration, waitForAvailableDuration, waitStartTime, writeStartTime;
  int bytesRead, bytesSent;
 
  // 1. Safety
  if (client == nullptr || !client->connected()) return false;

  // 2. Flush RX
  unsigned long startFlush = millis();
  while (client->available()) {
      client->read();
      if (millis() - startFlush > 10) break;
  }

  // 3. Open File
  FsFile f = sdfs.open(path);
  if (!f) {
    if (debug & DEBUG_WEB_SERVER) Serial.printf("404: %s\n", path);
    client->println("HTTP/1.1 404 Not Found");
    client->println("Connection: close\r\n");
    return false;
  }

  uint32_t fileSize = f.size();
 
  // 4. Send Headers
  client->println("HTTP/1.1 200 OK");
  client->print("Content-Type: ");
  client->println(getContentTypeSafe(path));
  client->print("Content-Length: ");
  client->println(fileSize);
  client->println("Connection: close");
  client->println();

  // 5. Stream Loop (Atomic - No Yield)
  const size_t chunkSize = 1460*4;

  uint8_t buffer[chunkSize];
 
  bool success = true;
  uint32_t lastProgress = millis();
  uint32_t totalSent = 0;
  waitStartTime = millis();
  while (f.available()) {
      // Check Connection
      if (!client->connected()) { success = false; break; }
      if (millis() - lastProgress > 5000) { success = false; break; }

      // Check Buffer
      int space = client->availableForWrite();
      if (space < 0) { success = false; break; }

      if (space >= chunkSize) {
          waitForAvailableDuration = millis() - waitStartTime;
          size_t toRead = chunkSize;
          if (toRead > (size_t)space) toRead = (size_t)space;
          if (toRead > chunkSize) toRead = chunkSize;

          startRead = millis(); 
          int n = f.read(buffer, toRead);
          readDuration = millis() - startRead;
          if (n < 0) { success = false; break; }
          //Serial.printf("%d bytes read\n", n);
          if (n > 0) {
              writeStartTime = millis();
              bytesSent = client->writeFully(buffer, n);
              writeDuration = millis() - writeStartTime;
              totalSent += bytesSent;
              Serial.printf("%d bytes read, %d bytes sent, %lu ms read, %lu ms write, %lu ms wait\n\r", n, bytesSent, readDuration, writeDuration, waitForAvailableDuration);
              lastProgress = millis();
              waitStartTime = millis();
              delay(2); // Allow buffer to flush
          }
      }
      else {
          
      }
      
  }

  if (debug & DEBUG_WEB_SERVER) Serial.printf("Sent %u/%u bytes: %s\n\r", totalSent, fileSize, path);

  if (success) {
      client->flush();
      //delay(150); // The "Green Bar" fix
  }
 
  f.close();
 
  return success;
}

unsigned char h2int(char c) {
    if (c >= '0' && c <= '9') {
        return ((unsigned char)c - '0');
    }
    if (c >= 'a' && c <= 'f') {
        return ((unsigned char)c - 'a' + 10);
    }
    if (c >= 'A' && c <= 'F') {
        return ((unsigned char)c - 'A' + 10);
    }
    return (0);
}

String urlDecode(String str) {
    String encodedString = str;
    String decodedString = "";
    char c;
    char code0;
    char code1;
    for (int i = 0; i < encodedString.length(); i++) {
        c = encodedString.charAt(i);
        if (c == '+') {
            decodedString += ' ';
        } else if (c == '%') {
            i++;
            code0 = encodedString.charAt(i);
            i++;
            code1 = encodedString.charAt(i);
            c = (h2int(code0) << 4) | h2int(code1);
            decodedString += c;
        } else {
            decodedString += c;
        }
    }
    return decodedString;
}

void processClientDataAlt(ClientState &state) {
  // alternative parsing method

  String request, requestLine;
  String contentType;
  String fileName;
  String stemp;
  Stream* streamTemp;
  int theFileLeftPos, theFileRightPos;
  int httpMethod, validRequest;
  int httpContentGroup;
  char c;
  uint32_t timeout, timeout2;
 
  state.lastRead = millis();
  state.emptyHeader=true;
  state.emptyRequest=true;

  // read in first line ie request
  timeout = millis()+1000;
  while ((true)&&(timeout>millis())) {
      int avail = state.client.available();
      if (avail <= 0) {
        return;
      }
      state.lastRead = millis();
      c = state.client.read();
      Serial.printf("%c", c);     // local echo
      state.HTTP_req[state.httpRequestIndex++] = c;
      //HTTP_req[httpRequestIndex++] = c;

      if (c == '\n') {
        state.emptyRequest=false;
        // Null terminate the request string
        if(state.httpRequestIndex < sizeof(state.HTTP_req)) state.HTTP_req[state.httpRequestIndex] = 0;
        break;
      }
      else if (state.httpRequestIndex >= sizeof(state.HTTP_req) - 1) {
        // Exceeded max length, ensure termination
        state.HTTP_req[sizeof(state.HTTP_req)-1] = 0;
        break;
      }
        
    }   // end of while(true)
  requestLine.reserve(sizeof(state.HTTP_req)+1);
  requestLine = state.HTTP_req;
 
  requestLine.trim();
  if (requestLine.length() > 0) {
    Serial.printf("requestline: %s\r\n", state.HTTP_req);

  // now process headers
  String authHeader = "";
  size_t contentLength = 0;
  String header;

  timeout2 = millis()+100;
  while ((true)&&(timeout2>millis())) {
   // now read in headers if they exist
   httpHeaderIndex=0;
   memset(HTTP_header, 0, sizeof(HTTP_header)); // clear out previous header
   timeout = millis()+100;
   while ((true)&&(timeout>millis())) {
    int avail = state.client.available();
    if (avail <= 0) {
        break;
    }
    state.lastRead = millis();
    c = state.client.read();
    Serial.printf("%c", c);     // local echo
    HTTP_header[httpHeaderIndex++] = c;

    if (c == '\n') {
        state.emptyHeader=false;
        break;
    }
    else if (httpHeaderIndex>=sizeof(HTTP_header)) break;
      
  }   // end of while(true)
  
    Serial.printf("header: %s\r\n", HTTP_header);
    header = HTTP_header;
    header.trim();
    if (header.length()==0) break;
    else {
      String lower = header; lower.toLowerCase();
      if (lower.startsWith("authorization:")) {
        int c = header.indexOf(':');
        if (c >= 0) {
          authHeader = header.substring(c+1);
          authHeader.trim();
        }
      }
      else if (lower.startsWith("content-length:")) {
         int c = header.indexOf(':');
         if (c >= 0) contentLength = (size_t) header.substring(c+1).toInt();
      }
   }
  }
  //---------------------------------------------------------------------------------------------------
  // require Basic auth
  //String expected = expectedAuthHeader(HTTP_USER, HTTP_PASS);
  //if (authHeader.length() == 0 || authHeader != expected) {
  //  Serial.println("unauthorised");
  //  requireAuth(&state.client);
  //  state.client.close();
  //  return;
 // }
  //---------------------------------------------------------------------------------------------------
  // parse method and rawPath
  int sp1 = requestLine.indexOf(' ');
  int sp2 = requestLine.indexOf(' ', sp1 + 1);
  if (sp1 < 0 || sp2 < 0) {
    state.client.println("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n");
    Serial.printf("no method or path, closing\r\n");
    state.client.close(); return;
  }
  String method = requestLine.substring(0, sp1);
  String rawPath = requestLine.substring(sp1 + 1, sp2);
  //---------------------------------------------------------------------------------------------------
  // check path is suitable
  if (rawPath.indexOf("..") >= 0) {
    state.client.println("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n");
    Serial.println("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n");
    state.client.close(); return;
  }
  // server endpoint implementation

  if (method == "GET") {
    // 1. Use the 'rawPath' we already parsed at the top
    String fullPath = rawPath;

    // 2. Strip Query Parameters (CRITICAL for Publii ?v=... links)
    int qIndex = fullPath.indexOf('?');
    if (qIndex >= 0) {
        fullPath = fullPath.substring(0, qIndex);
    }
    // 3. URL Decode (Handle spaces/%20)
    fullPath = urlDecode(fullPath);
    // 4. Smart Path Mapping
    // If asking for root, serve index.html
    if (fullPath == "/" || fullPath == "/website" || fullPath == "/website/") {
        fullPath = "/website/index.html";
    }
    Serial.printf("Serving: %s\r\n", fullPath.c_str());
    // 5. Call Robust serveFile (Pass 'sd' by Ref, 'state.client' by Value)
    // Note: We use 'sd' directly here assuming it is the global SdFs object
    bool sentOK = serveFile(sd, &state.client, fullPath.c_str());

    if (sentOK) {
        Serial.println("File Sent.");
    } else {
        Serial.println("File Send Failed.");
        // If it failed, ensure we kill the connection
        state.client.stop();
    }
    
    state.closed = true;
  }
  else {
    state.client.println("HTTP/1.1 404 Not Found\r\nConnection: close\r\n");
    Serial.println("HTTP/1.1 404 Not Found\r\nConnection: close\r\n");
  }

  delay(1);
  //state.client.close();

  }
  else {
    // nothing here, no actual request found
    state.emptyLine = true;
  }
  // now close connection and reset request strings etc
  Serial.println("preparing to close connection");   
 
  state.httpRequestIndex=0;
  memset(state.HTTP_req, 0, sizeof(state.HTTP_req)); 
  httpHeaderIndex=0;
  memset(HTTP_header, 0, sizeof(HTTP_header));  // clear the request string
 
} // end if process clients
// --------------------------------------------------------------------------
//  Main Program
// --------------------------------------------------------------------------

// Program setup.
void setup() {
  Serial.begin(115200);
  while (!Serial && (millis() < 4000)) {
    // Wait for Serial
  }
  Serial.printf("Starting...\r\n");

  // Unlike the Arduino API (which you can still use), QNEthernet uses
  // the Teensy's internal MAC address by default, so we can retrieve
  // it here
  uint8_t mac[6];
  Ethernet.macAddress(mac);  // This is informative; it retrieves, not sets
  Serial.printf("MAC = %02x:%02x:%02x:%02x:%02x:%02x\r\n",
         mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

  // Add listeners
  // It's important to add these before doing anything with Ethernet
  // so no events are missed.

  // Listen for link changes
  Ethernet.onLinkState([](bool state) {
    Serial.printf("[Ethernet] Link %s\r\n", state ? "ON" : "OFF");
  });

  // Listen for address changes
  Ethernet.onAddressChanged([]() {
    IPAddress ip = Ethernet.localIP();
    bool hasIP = (ip != INADDR_NONE);
    if (hasIP) {
      Serial.println("[Ethernet] Address changed:");

      Serial.printf("    Local IP = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      ip = Ethernet.subnetMask();
      Serial.printf("    Subnet   = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      ip = Ethernet.gatewayIP();
      Serial.printf("    Gateway  = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      ip = Ethernet.dnsServerIP();
      if (ip != INADDR_NONE) {  // May happen with static IP
        Serial.printf("    DNS      = %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
      }
    } else {
      Serial.println("[Ethernet] Address changed: No IP address");
    }
  });

  if (initEthernet()) {
    // Start the server
    Serial.printf("Starting server on port %u...", kServerPort);
    server.begin();
    Serial.printf("%s\r\n", (server) ? "Done." : "FAILED!");
  }
  // initialise SD card
  if (!sd.begin(SD_CONFIG)){
    Serial.println("failed, is a card inserted?");
    while(1);
  }



}

bool initEthernet() {
  // DHCP
  if (staticIP == INADDR_NONE) {
    Serial.println("Starting Ethernet with DHCP...");
    if (!Ethernet.begin()) {
      Serial.println("Failed to start Ethernet");
      return false;
    }

    // We can choose not to wait and rely on the listener to tell us
    // when an address has been assigned
    if (kDHCPTimeout > 0) {
      printf("Waiting for IP address...\r\n");
      if (!Ethernet.waitForLocalIP(kDHCPTimeout)) {
        printf("No IP address yet\r\n");
        // We may still get an address later, after the timeout,
        // so continue instead of returning
      }
    }
  } else {
    // Static IP
    printf("Starting Ethernet with static IP...\r\n");
    if (!Ethernet.begin(staticIP, subnetMask, gateway)) {
      printf("Failed to start Ethernet\r\n");
      return false;
    }

    // When setting a static IP, the address is changed immediately,
    // but the link may not be up; optionally wait for the link here
    if (kLinkTimeout > 0) {
      printf("Waiting for link...\r\n");
      if (!Ethernet.waitForLink(kLinkTimeout)) {
        printf("No link yet\r\n");
        // We may still see a link later, after the timeout, so
        // continue instead of returning
      }
    }
  }

  return true;
}

// Main program loop.
void loop() {
  EthernetClient client = server.accept();
  if (client) {
    // We got a connection!
    IPAddress ip = client.remoteIP();
    printf("Client connected: %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
    clients.emplace_back(std::move(client));
    printf("Client count: %zu\r\n", clients.size());
  }

  // Process data from each client
  for (ClientState& state : clients) {  // Use a reference so we don't copy
    if (!state.client.connected()) {
      state.closed = true;
      continue;
    }
    // Check if we need to force close the client
    if (state.outputClosed) {
      if (millis() - state.closedTime >= kShutdownTimeout) {
        IPAddress ip = state.client.remoteIP();
        printf("Client shutdown timeout: %u.%u.%u.%u\r\n",
               ip[0], ip[1], ip[2], ip[3]);
        state.client.close();
        state.closed = true;
        continue;
      }
    } else {
      if (millis() - state.lastRead >= kClientTimeout) {
        IPAddress ip = state.client.remoteIP();
        printf("Client timeout: %u.%u.%u.%u\r\n", ip[0], ip[1], ip[2], ip[3]);
        state.client.close();
        state.closed = true;
        continue;
      }
    }
    processClientDataAlt(state);
  }

  // Clean up all the closed clients
  size_t size = clients.size();
  clients.erase(
      std::remove_if(clients.begin(), clients.end(),
                     [](const ClientState& state) { return state.closed; }),
      clients.end());
  if (clients.size() != size) {
    printf("New client count: %zu\r\n", clients.size());
  }
}
 
Example debug dump for a jpg

Client connected: 192.168.2.100
Client count: zu
GET /website/media/posts/2/homepage_2.43.jpg HTTP/1.1
requestline: GET /website/media/posts/2/homepage_2.43.jpg HTTP/1.1

Host: 192.168.2.177
header: Host: 192.168.2.177

Connection: keep-alive
header: Connection: keep-alive

Authorization: Basic YWRtaW46SGlQZXJEYXE=
header: Authorization: Basic YWRtaW46SGlQZXJEYXE=

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
header: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36

Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
header: Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8

Referer: http://192.168.2.177/website/basic-website-navigation.html
header: Referer: http://192.168.2.177/website/basic-website-navigation.html

Accept-Encoding: gzip, deflate
header: Accept-Encoding: gzip, deflate

Accept-Language: en-US,en;q=0.9
header: Accept-Language: en-US,en;q=0.9


header:

Serving: /website/media/posts/2/homepage_2.43.jpg
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 488 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 4 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 5 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 2 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 5 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 4 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 2 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 4 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 3 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 2 ms wait
5538 bytes read, 5538 bytes sent, 0 ms read, 0 ms write, 3 ms wait
Sent 186578/186578 bytes: /website/media/posts/2/homepage_2.43.jpg
File Sent.
preparing to close connection
New client count: zu
 
After a bit more tweaking, the performance has been improved quite a lot:

Threshold of available space before write dropped to 1460,

delay(2); after write changed to yield();


C++:
  while (f.available()) {
      // Check Connection
      if (!client->connected()) { success = false; break; }
      if (millis() - lastProgress > 5000) { success = false; break; }

      // Check Buffer
      yield();
      int space = client->availableForWrite();
      if (space < 0) { success = false; break; }

      if (space >= 1460) {
          waitForAvailableDuration = millis() - waitStartTime;
          size_t toRead = chunkSize;
          if (toRead > (size_t)space) toRead = (size_t)space;
          if (toRead > chunkSize) toRead = chunkSize;

          startRead = millis(); 
          int n = f.read(buffer, toRead);
          readDuration = millis() - startRead;
          if (n < 0) { success = false; break; }
          //Serial.printf("%d bytes read\n", n);
          if (n > 0) {
              writeStartTime = millis();
              bytesSent = client->writeFully(buffer, n);
              writeDuration = millis() - writeStartTime;
              totalSent += bytesSent;
              Serial.printf("%d bytes read, %d bytes sent, %lu ms read, %lu ms write, %lu ms wait\n\r", n, bytesSent, readDuration, writeDuration, waitForAvailableDuration);
              lastProgress = millis();
              waitStartTime = millis();
              //delay(2); // Allow buffer to flush
              yield();
          }
      }
      else {
          
      }
      
  }


Resulting performance log. Note initial 450 or so ms wait is no more.

------------------------------------------------------------------------

Client connected: 192.168.2.100
Client count: zu
GET /website/media/posts/2/homepage_2.43.jpg HTTP/1.1
requestline: GET /website/media/posts/2/homepage_2.43.jpg HTTP/1.1

Host: 192.168.2.177
header: Host: 192.168.2.177

Connection: keep-alive
header: Connection: keep-alive

Pragma: no-cache
header: Pragma: no-cache

Cache-Control: no-cache
header: Cache-Control: no-cache

Authorization: Basic YWRtaW46SGlQZXJEYXE=
header: Authorization: Basic YWRtaW46SGlQZXJEYXE=

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
header: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36

Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
header: Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8

Referer: http://192.168.2.177/website/basic-website-navigation.html
header: Referer: http://192.168.2.177/website/basic-website-navigation.html

Accept-Encoding: gzip, deflate
header: Accept-Encoding: gzip, deflate

Accept-Language: en-US,en;q=0.9
header: Accept-Language: en-US,en;q=0.9


header:

Serving: /website/media/posts/2/homepage_2.43.jpg
5752 bytes read, 5752 bytes sent, 1 ms read, 0 ms write, 0 ms wait
4380 bytes read, 4380 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 0 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 1 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 0 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 0 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 2 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 0 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 1 ms wait
5840 bytes read, 5840 bytes sent, 1 ms read, 0 ms write, 0 ms wait
5840 bytes read, 5840 bytes sent, 0 ms read, 0 ms write, 1 ms wait
1246 bytes read, 1246 bytes sent, 0 ms read, 0 ms write, 1 ms wait
Sent 186578/186578 bytes: /website/media/posts/2/homepage_2.43.jpg
File Sent.
preparing to close connection
New client count: zu
 
Back
Top