This is by default and by design, and I am not sure you can solve this with bash
. You can certainly solve this with zsh
(it has select() syscall module) but maybe shell is not the right language for the job at this point.
The issue is simple as everything in unix is, but actual understanding and solution requires some deeper thinking and practical knowledge.
Cause of the effect is blocking flag set on the file descriptor, which is done by the system kernel by default when you open any VFS object.
In unix, process cooperation is synchronized by blocking at IO boundaries, which is very intuitive and makes everything very elegant and simple and makes naive and simplistic programming of beginner and intermediate application programmers "just work".
When you open object in question (file, fifo or whatever), the blocking flag set on it ensures, that any read form the descriptor immediately blocks the whole process when there is no data to be read. This block is unblocked only and only after some data is filled into the object from the other side (in case of pipe).
Regular files are an "exception", at least when compared to pipe, as from the point of IO subsystem they "never block" (even if they actually do - ie unsetting blocking flag on file fd has no effect, as process block happens deeper in kernel's storage fd read routine). From process POV disk read is always instant and in zero time without blocking (even though system clocks actually jump forward during such read).
This design has two effects you are observing:
First, you never really observe effects of IO blocking when juggling just regular files from shell (as they never block for real, as we explained above).
Second, once you get blocked in shell on read() from pipe, like you've got here, your process is essentially stuck in block forever as this kind of blocking is "for real", at least until more data is not filled in from the other side of the pipe. Your process does not even run and consume CPU time in that state, it is kernel who holds it blocked from outside, until more data arrives, and thus process timeout routines cannot even run either (as that requires process to be consuming CPU time ie running).
Your process will remain blocked at least until you fill pipe with sufficient amount of data to be read, then it will ublocked briefly, until all the data is consumed again and process is blocked again.
If you ponder about it carefully this is actually what makes pipes in shell work.
Have you ever wondered how come that complex shell pipeline adapts to fast or slow programs somehow on it's own? This is the mechanism that makes it work. Fast generators spewing output fast will make next program in pipeline read it faster, and slow data generator in pipeline will make any subsequent program in pipeline read/run slower - everything is rate limited by the pipes blocking on data, synchonizing whole pipe as if by magic.
EDIT: further clarification
How to get out of it?
There is no easy way. Not in bash as far as I know.
Easiest one is to ponder more about the problem and redesign it different way.
Due nature of blocking explained above, the most simple is to understand imposed design constraint for shell programs: only have one main input stream.
This will make shell program robust enough to deal with both file input (not a problem) and pipe.
Reading from multiple pipes (ie. even two) will make your program block naturally until both of them have data, so if you can ensure that both pipes are full of data at all times this will work. Unfortunately this rarely works: the moment reading from pipes becomes intertwined and interleaved you have problem with pipes filling in random order - especially if reads are dependent first pipe to become empty will stall you whole processing. We call such situation deadlock.
You can solve problem of reading from multiple pipes by removing the blocking flag from file descriptors in question, but now you have IO scheduling and data multiplexing problem, which require properly equipped language to deal with.
I am afraid bash is not equipped well enough for that and even if it is you now need to learn more how this stuff works then.
cat > fifo
running? Because, well, it works for me with the timeout option as long as both the pipes are open on the other end for the whole time the reader runs. (They have to be open on the write end when the reader starts, since otherwise the open blocks; and if the writers disappear, all reads start to return zero-length reads, basically turning the reader into a busyloop.) – ilkkachu Jun 13 '22 at 17:08echo > pipe
) – Roger Miranda Perez Jun 13 '22 at 17:12